diff --git a/README.md b/README.md
index 8169dfc..f25cfd4 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,13 @@ It is designed for practical daily work: IDEs, chats, notes, CRM fields, email,
## Latest Changes
+### v0.3.6
+
+- Fixed permissions onboarding recovery for existing installs so updates do not reopen the full permissions screen when required macOS access is already granted.
+- Switched the app icon set from Lucide to Phosphor through a local icon facade for softer, more consistent product UI icons.
+- Added a macOS Vite/esbuild runner workaround for development and release builds from external drives.
+- Kept local STT failures separate from API-key authentication errors when a local model is selected.
+
### 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.
diff --git a/bun.lock b/bun.lock
index cfb80c0..9776baa 100644
--- a/bun.lock
+++ b/bun.lock
@@ -1,10 +1,10 @@
{
"lockfileVersion": 1,
- "configVersion": 0,
"workspaces": {
"": {
"name": "talk-flow",
"dependencies": {
+ "@phosphor-icons/react": "^2.1.10",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-autostart": "^2.5.1",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
@@ -15,7 +15,6 @@
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-store": "^2.4.2",
"@tauri-apps/plugin-updater": "^2.10.1",
- "lucide-react": "^0.577.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
},
@@ -135,6 +134,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+ "@phosphor-icons/react": ["@phosphor-icons/react@2.1.10", "", { "peerDependencies": { "react": ">= 16.8", "react-dom": ">= 16.8" } }, "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA=="],
+
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
@@ -361,8 +362,6 @@
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
- "lucide-react": ["lucide-react@0.577.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A=="],
-
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
diff --git a/docs/release/review-v0.3.6.md b/docs/release/review-v0.3.6.md
new file mode 100644
index 0000000..274170e
--- /dev/null
+++ b/docs/release/review-v0.3.6.md
@@ -0,0 +1,61 @@
+# Release Review v0.3.6
+
+## Release
+
+- Version: 0.3.6
+- Release branch: release/v0.3.6
+- Target tag: v0.3.6
+- Reviewer: Codex
+- Date: 2026-07-02
+
+## Scope
+
+- Key changes included in this release:
+ - Recover completed permissions onboarding for existing installs when required macOS permissions are already granted.
+ - Replace Lucide icons with Phosphor icons through a local icon facade.
+ - Add a Vite runner that copies the macOS esbuild binary to the system temp directory before dev/build, avoiding `write EPIPE` failures when the repo lives on an external drive.
+ - Preserve the local-model API-key fix and widget notice alignment fix from the hotfix work.
+- User-facing changes:
+ - Updates should no longer reopen the full permissions screen for users who already granted microphone and Accessibility access.
+ - Local STT mode should no longer surface API-key wording for local runtime failures.
+ - Settings and widget UI use the softer Phosphor icon set.
+ - `bun run tauri dev` works from `/Volumes/KINGSTON` without manual `ESBUILD_BINARY_PATH`.
+- Risky areas:
+ - Permissions onboarding recovery relies on existing history plus current OS permission checks.
+ - Icon migration touches many UI imports, though through one facade.
+ - Local macOS updater signing requires `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`; the local environment did not provide it.
+
+## Checks run
+
+- `bun run check:release`: passed.
+- `bun run build:release:macos`: app bundle and updater tarball built, then failed at updater signing because the local environment did not provide the private-key password.
+- Native/GitHub Windows build: pending GitHub Actions tag build.
+- Native/GitHub Linux build: pending GitHub Actions tag build.
+- Additional manual checks:
+ - `bun run dev -- --host 127.0.0.1`: passed after the Vite/esbuild runner fix.
+ - `bunx tsc --noEmit`: passed during icon rollback.
+ - Verified `lucide-react`, `@tabler/icons-react`, and `@hugeicons/*` are absent from app dependencies and source.
+
+## Manual review
+
+- Hotkey flow: smoke test passed through `src/windows/widget/services/hotkeyFsm.test.js`.
+- Onboarding permissions: reviewed startup gating in `SettingsApp`; system-audio permission is no longer required for the initial settings app gate, and existing installs with history can recover the completed flag.
+- Widget position and notice behavior: reviewed notice icon facade usage and centered icon layout.
+- Transcription quality and short-utterance handling: unchanged in this release.
+- README refreshed: yes, `README.md` includes `v0.3.6` notes.
+
+## Findings
+
+- Blockers:
+ - None for GitHub Actions release, assuming repository signing secrets include the updater private key password.
+- Non-blocking issues:
+ - Local `bun run build:release:macos` cannot complete updater signing without `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`.
+ - Production frontend bundle remains above the default Vite 500 kB warning threshold.
+- Follow-ups after release:
+ - Consider documenting the local updater signing password requirement for macOS release builds.
+ - Consider code-splitting settings tabs or model assets to reduce the main frontend bundle size.
+
+## Decision
+
+- Ready for `main` merge: yes.
+- Ready for tag publish: yes, with GitHub Actions signing secrets expected to complete updater signing.
diff --git a/package.json b/package.json
index bc6a672..54d55eb 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "talkis",
"private": true,
- "version": "0.3.5",
+ "version": "0.3.6",
"license": "AGPL-3.0-or-later",
"type": "module",
"packageManager": "bun@1.2.13",
@@ -9,8 +9,8 @@
"prepare:ffmpeg-sidecar": "bun scripts/prepare-ffmpeg-sidecar.mjs",
"prepare:stt-sidecar": "bun scripts/prepare-stt-sidecar.mjs",
"prepare:sidecars": "bun run prepare:ffmpeg-sidecar && bun run prepare:stt-sidecar",
- "dev": "vite",
- "build": "tsc && vite build",
+ "dev": "bun scripts/run-vite.mjs",
+ "build": "tsc && bun scripts/run-vite.mjs build",
"check": "bun run prepare:sidecars && bunx tsc --noEmit && cargo check --manifest-path src-tauri/Cargo.toml",
"test": "bun test",
"smoke:hotkey": "bun test ./src/windows/widget/services/hotkeyFsm.test.js",
@@ -21,12 +21,13 @@
"build:release:macos": "bun scripts/build-release.mjs macos",
"build:release:windows": "bun scripts/build-release.mjs windows",
"build:release:linux": "bun scripts/build-release.mjs linux",
- "preview": "vite preview",
+ "preview": "bun scripts/run-vite.mjs preview",
"tauri": "tauri",
"logs": "tail -f ~/.talkis/talkis.log",
"logs:clear": "rm -f ~/.talkis/talkis.log"
},
"dependencies": {
+ "@phosphor-icons/react": "^2.1.10",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-autostart": "^2.5.1",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
@@ -37,7 +38,6 @@
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-store": "^2.4.2",
"@tauri-apps/plugin-updater": "^2.10.1",
- "lucide-react": "^0.577.0",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
diff --git a/scripts/run-vite.mjs b/scripts/run-vite.mjs
new file mode 100644
index 0000000..cd93f8c
--- /dev/null
+++ b/scripts/run-vite.mjs
@@ -0,0 +1,56 @@
+import { spawn } from "node:child_process";
+import { existsSync, mkdirSync, copyFileSync, chmodSync } from "node:fs";
+import { dirname, join } from "node:path";
+import { tmpdir } from "node:os";
+import { createRequire } from "node:module";
+
+const require = createRequire(import.meta.url);
+const rootDir = dirname(dirname(new URL(import.meta.url).pathname));
+
+function resolvePackageFile(packageName, relativePath) {
+ return join(dirname(require.resolve(`${packageName}/package.json`)), relativePath);
+}
+
+function prepareEsbuildBinary() {
+ if (process.platform !== "darwin") return undefined;
+
+ const source = resolvePackageFile("esbuild", "bin/esbuild");
+ if (!existsSync(source)) return undefined;
+
+ const targetDir = join(tmpdir(), "talkis-esbuild");
+ const target = join(targetDir, `esbuild-${process.platform}-${process.arch}`);
+
+ mkdirSync(targetDir, { recursive: true });
+ copyFileSync(source, target);
+ chmodSync(target, 0o755);
+
+ return target;
+}
+
+function resolveViteBin() {
+ const binName = process.platform === "win32" ? "vite.cmd" : "vite";
+ return join(rootDir, "node_modules", ".bin", binName);
+}
+
+const env = { ...process.env };
+const esbuildBinary = prepareEsbuildBinary();
+
+if (esbuildBinary) {
+ env.ESBUILD_BINARY_PATH = esbuildBinary;
+}
+
+const child = spawn(resolveViteBin(), process.argv.slice(2), {
+ cwd: rootDir,
+ env,
+ stdio: "inherit",
+ shell: process.platform === "win32",
+});
+
+child.on("exit", (code, signal) => {
+ if (signal) {
+ process.kill(process.pid, signal);
+ return;
+ }
+
+ process.exit(code ?? 1);
+});
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index d30cb48..a6262de 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -4969,7 +4969,7 @@ dependencies = [
[[package]]
name = "talkis"
-version = "0.3.5"
+version = "0.3.6"
dependencies = [
"base64 0.22.1",
"block2 0.6.2",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index a63cb4f..cf08ea9 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "talkis"
-version = "0.3.5"
+version = "0.3.6"
description = "Talkis - Voice to Text Desktop Widget"
authors = ["trixter"]
license = "AGPL-3.0-or-later"
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index e668a1d..a8d538b 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -2,7 +2,7 @@
"$schema": "https://schema.tauri.app/config/2",
"productName": "Talkis",
"mainBinaryName": "Talkis",
- "version": "0.3.5",
+ "version": "0.3.6",
"identifier": "com.trixter.talkis",
"build": {
"beforeDevCommand": "bun run prepare:sidecars && bun run dev",
diff --git a/src/components/Dropdown.tsx b/src/components/Dropdown.tsx
index 5c0ac35..f5e5bed 100644
--- a/src/components/Dropdown.tsx
+++ b/src/components/Dropdown.tsx
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from "react";
-import { Check, ChevronDown } from "lucide-react";
+import { IconCheck, IconChevronDown } from "../lib/icons";
export interface DropdownOption {
value: string;
@@ -59,9 +59,9 @@ export function Dropdown({
{current?.label ?? value}
-
@@ -113,7 +113,7 @@ export function Dropdown({
onMouseLeave={(e) => (e.currentTarget.style.background = active ? "var(--dropdown-active)" : "transparent")}
>
{option.label}
- {active && }
+ {active && }
);
})}
diff --git a/src/components/LocalLlmModels.tsx b/src/components/LocalLlmModels.tsx
index 3ed93fb..560dbd9 100644
--- a/src/components/LocalLlmModels.tsx
+++ b/src/components/LocalLlmModels.tsx
@@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
-import { AlertCircle, Check, Download, HardDrive, Loader2, MemoryStick, Trash2, X } from "lucide-react";
+import { IconAlertCircle, IconCheck, IconDownload, IconDeviceDesktopCog, IconLoader2, IconCpu, IconTrash, IconX } from "../lib/icons";
import { AppSettings } from "../lib/store";
import { useI18n } from "../lib/i18n";
@@ -225,7 +225,7 @@ export function LocalLlmModels({
border: "1px solid var(--border-subtle)",
}}
>
-
+
{t("localLlm.required")}
)}
@@ -344,7 +344,7 @@ export function LocalLlmModels({
aria-label={t("localLlm.diskSize", { size: model.size_label })}
style={{ display: "flex", alignItems: "center", gap: 6 }}
>
-
+
{model.size_label}
@@ -354,7 +354,7 @@ export function LocalLlmModels({
aria-label={t("localLlm.ramRequired", { ram: model.min_ram_gb })}
style={{ display: "flex", alignItems: "center", gap: 6 }}
>
-
+
{t("localLlm.ramShort", { ram: model.min_ram_gb })}
@@ -442,7 +442,7 @@ export function LocalLlmModels({
color: "var(--text-hi)",
}}
>
-
+
{t("localLlm.cancel")}
) : (
@@ -454,7 +454,7 @@ export function LocalLlmModels({
color: "var(--accent-contrast)",
}}
>
-
+
{t("localLlm.download")}
)
@@ -472,13 +472,13 @@ export function LocalLlmModels({
}}
>
{isBusy ? (
-
) : (
-
+
)}
{t("localLlm.select")}
@@ -492,7 +492,7 @@ export function LocalLlmModels({
color: "var(--text-hi)",
}}
>
-
+
{t("localLlm.delete")}
>
diff --git a/src/components/PermissionScreen.tsx b/src/components/PermissionScreen.tsx
index 4ea93b6..4ae0b92 100644
--- a/src/components/PermissionScreen.tsx
+++ b/src/components/PermissionScreen.tsx
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import { openPath } from "@tauri-apps/plugin-opener";
-import { Mic, Keyboard, Check, AlertCircle, Volume2 } from "lucide-react";
+import { IconMicrophone, IconKeyboard, IconCheck, IconAlertCircle, IconVolume } from "../lib/icons";
import {
PermissionStatus,
checkAccessibilityPermission,
@@ -66,7 +66,7 @@ function PermissionRow({
flexShrink: 0,
}}
>
- {isGranted ? : icon}
+ {isGranted ? : icon}
@@ -452,7 +452,7 @@ export function PermissionScreen({ onComplete }: PermissionScreenProps) {
border: "1px solid var(--danger-border)",
}}
>
-
}
+ icon={
}
title={t("permission.mic.title")}
description={t("permission.mic.desc")}
status={micStatus}
@@ -493,7 +493,7 @@ export function PermissionScreen({ onComplete }: PermissionScreenProps) {
{requiresSystemAudio && (
}
+ icon={
}
title={t("permission.systemAudio.title")}
description={t("permission.systemAudio.desc")}
status={systemAudioStatus}
@@ -503,7 +503,7 @@ export function PermissionScreen({ onComplete }: PermissionScreenProps) {
)}
}
+ icon={
}
title={pastePermissionTitle}
description={pastePermissionDescription}
status={
@@ -540,7 +540,7 @@ export function PermissionScreen({ onComplete }: PermissionScreenProps) {
border: "1px solid var(--border-subtle)",
}}
>
-
-
+
{open && (
diff --git a/src/components/SlideOverModal.tsx b/src/components/SlideOverModal.tsx
index 14f6260..545944c 100644
--- a/src/components/SlideOverModal.tsx
+++ b/src/components/SlideOverModal.tsx
@@ -1,5 +1,5 @@
import { useEffect, useState, type ReactNode } from "react";
-import { X } from "lucide-react";
+import { IconX } from "../lib/icons";
import { useI18n } from "../lib/i18n";
@@ -98,7 +98,7 @@ export function SlideOverModal({
aria-label={t("summary.close")}
style={{ width: 32, minWidth: 32, minHeight: 32, padding: 0, justifyContent: "center", flexShrink: 0 }}
>
-
+
diff --git a/src/components/SummaryModal.tsx b/src/components/SummaryModal.tsx
index e8a4509..a1327ff 100644
--- a/src/components/SummaryModal.tsx
+++ b/src/components/SummaryModal.tsx
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from "react";
import { listen } from "@tauri-apps/api/event";
-import { Check, Clipboard, Pencil, Trash2 } from "lucide-react";
+import { IconCheck, IconClipboard, IconPencil, IconTrash } from "../lib/icons";
import { SETTINGS_UPDATED_EVENT } from "../lib/hotkeyEvents";
import {
@@ -295,9 +295,9 @@ export function SummaryModal({
: , onSelect: () => void copy(item) },
- { key: "edit", label: t("summary.edit"), icon: , onSelect: () => startEdit(item) },
- { key: "delete", label: t("summary.delete"), icon: , danger: true, onSelect: () => void remove(item) },
+ { key: "copy", label: copiedId === item.id ? t("summary.copied") : t("summary.copy"), icon: copiedId === item.id ? : , onSelect: () => void copy(item) },
+ { key: "edit", label: t("summary.edit"), icon: , onSelect: () => startEdit(item) },
+ { key: "delete", label: t("summary.delete"), icon: , danger: true, onSelect: () => void remove(item) },
] as RowActionItem[]}
/>
diff --git a/src/components/UserPanel.tsx b/src/components/UserPanel.tsx
index f891c56..8cf6bbd 100644
--- a/src/components/UserPanel.tsx
+++ b/src/components/UserPanel.tsx
@@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useRef } from "react";
import { openUrl } from "@tauri-apps/plugin-opener";
import { listen } from "@tauri-apps/api/event";
import { onOpenUrl } from "@tauri-apps/plugin-deep-link";
-import { LogOut, User, Crown } from "lucide-react";
+import { IconLogout, IconUser, IconCrown } from "../lib/icons";
import {
beginCloudAuthFlow,
@@ -221,7 +221,7 @@ export function UserPanel() {
cancelLocalAuthPolling();
setProfile(null);
await cloudLogout();
- logInfo("USER_PANEL", "User logged out");
+ logInfo("USER_PANEL", "IconUser logged out");
};
if (loading) {
@@ -247,7 +247,7 @@ export function UserPanel() {
@@ -274,7 +274,7 @@ function ProfileRow({ profile, onLogout }: { profile: CloudProfile; onLogout: ()
style={{ width: "100%", height: "100%", borderRadius: "50%", objectFit: "cover" }}
/>
) : (
-
+
)}
@@ -284,7 +284,7 @@ function ProfileRow({ profile, onLogout }: { profile: CloudProfile; onLogout: ()
{profile.user.email}
);
@@ -295,7 +295,7 @@ function SubscriptionCTA({ onActivate }: { onActivate: () => void }) {
return (
-
+
{t("userPanel.cta.title")}
diff --git a/src/lib/icons.ts b/src/lib/icons.ts
new file mode 100644
index 0000000..1d37649
--- /dev/null
+++ b/src/lib/icons.ts
@@ -0,0 +1,180 @@
+import type {
+ ComponentPropsWithoutRef,
+ ForwardRefExoticComponent,
+ RefAttributes,
+} from "react";
+
+import { SlidersHorizontalIcon as RawIconAdjustmentsHorizontal } from "@phosphor-icons/react/SlidersHorizontal";
+import { WarningCircleIcon as RawIconAlertCircle } from "@phosphor-icons/react/WarningCircle";
+import { WarningIcon as RawIconAlertTriangle } from "@phosphor-icons/react/Warning";
+import { LightningIcon as RawIconBolt } from "@phosphor-icons/react/Lightning";
+import { BriefcaseIcon as RawIconBriefcase } from "@phosphor-icons/react/Briefcase";
+import { BroadcastIcon as RawIconBroadcast } from "@phosphor-icons/react/Broadcast";
+import { CheckIcon as RawIconCheck } from "@phosphor-icons/react/Check";
+import { CaretDownIcon as RawIconChevronDown } from "@phosphor-icons/react/CaretDown";
+import { ClipboardIcon as RawIconClipboard } from "@phosphor-icons/react/Clipboard";
+import { CloudIcon as RawIconCloud } from "@phosphor-icons/react/Cloud";
+import { CodeIcon as RawIconCode } from "@phosphor-icons/react/Code";
+import { CopyIcon as RawIconCopy } from "@phosphor-icons/react/Copy";
+import { CpuIcon as RawIconCpu } from "@phosphor-icons/react/Cpu";
+import { CrownIcon as RawIconCrown } from "@phosphor-icons/react/Crown";
+import { DesktopIcon as RawIconDeviceDesktop } from "@phosphor-icons/react/Desktop";
+import { GearIcon as RawIconDeviceDesktopCog } from "@phosphor-icons/react/Gear";
+import { DotsThreeIcon as RawIconDots } from "@phosphor-icons/react/DotsThree";
+import { DownloadSimpleIcon as RawIconDownload } from "@phosphor-icons/react/DownloadSimple";
+import { ArrowSquareOutIcon as RawIconExternalLink } from "@phosphor-icons/react/ArrowSquareOut";
+import { FileAudioIcon as RawIconFileMusic } from "@phosphor-icons/react/FileAudio";
+import { GaugeIcon as RawIconGauge } from "@phosphor-icons/react/Gauge";
+import { HeadphonesIcon as RawIconHeadphones } from "@phosphor-icons/react/Headphones";
+import { QuestionIcon as RawIconHelpCircle } from "@phosphor-icons/react/Question";
+import { HouseIcon as RawIconHome } from "@phosphor-icons/react/House";
+import { InfoIcon as RawIconInfoCircle } from "@phosphor-icons/react/Info";
+import { KeyboardIcon as RawIconKeyboard } from "@phosphor-icons/react/Keyboard";
+import { TranslateIcon as RawIconLanguage } from "@phosphor-icons/react/Translate";
+import { ListChecksIcon as RawIconListCheck } from "@phosphor-icons/react/ListChecks";
+import { SpinnerGapIcon as RawIconLoader2 } from "@phosphor-icons/react/SpinnerGap";
+import { SignOutIcon as RawIconLogout } from "@phosphor-icons/react/SignOut";
+import { EnvelopeIcon as RawIconMail } from "@phosphor-icons/react/Envelope";
+import { ChatCircleIcon as RawIconMessage } from "@phosphor-icons/react/ChatCircle";
+import { MicrophoneIcon as RawIconMicrophone } from "@phosphor-icons/react/Microphone";
+import { MoonIcon as RawIconMoon } from "@phosphor-icons/react/Moon";
+import { PencilSimpleIcon as RawIconPencil } from "@phosphor-icons/react/PencilSimple";
+import { PhoneCallIcon as RawIconPhoneCall } from "@phosphor-icons/react/PhoneCall";
+import { PlayIcon as RawIconPlayerPlay } from "@phosphor-icons/react/Play";
+import { PlusIcon as RawIconPlus } from "@phosphor-icons/react/Plus";
+import { ArrowClockwiseIcon as RawIconRefresh } from "@phosphor-icons/react/ArrowClockwise";
+import { ArrowsClockwiseIcon as RawIconRotate2 } from "@phosphor-icons/react/ArrowsClockwise";
+import { MagnifyingGlassIcon as RawIconSearch } from "@phosphor-icons/react/MagnifyingGlass";
+import { HardDrivesIcon as RawIconServer } from "@phosphor-icons/react/HardDrives";
+import { SparkleIcon as RawIconSparkles } from "@phosphor-icons/react/Sparkle";
+import { SpeakerHighIcon as RawIconSpeakerphone } from "@phosphor-icons/react/SpeakerHigh";
+import { SquareIcon as RawIconSquare } from "@phosphor-icons/react/Square";
+import { SunIcon as RawIconSun } from "@phosphor-icons/react/Sun";
+import { TargetIcon as RawIconTargetArrow } from "@phosphor-icons/react/Target";
+import { TrashIcon as RawIconTrash } from "@phosphor-icons/react/Trash";
+import { TextTIcon as RawIconTypography } from "@phosphor-icons/react/TextT";
+import { UserIcon as RawIconUser } from "@phosphor-icons/react/User";
+import { SpeakerHighIcon as RawIconVolume } from "@phosphor-icons/react/SpeakerHigh";
+import { XIcon as RawIconX } from "@phosphor-icons/react/X";
+
+export interface IconProps
+ extends Omit, "stroke">,
+ RefAttributes {
+ alt?: string;
+ color?: string;
+ mirrored?: boolean;
+ size?: number | string;
+ stroke?: number | string;
+ title?: string;
+ weight?: "thin" | "light" | "regular" | "bold" | "fill" | "duotone";
+}
+
+export type Icon = ForwardRefExoticComponent;
+
+const IconAdjustmentsHorizontal = RawIconAdjustmentsHorizontal as Icon;
+const IconAlertCircle = RawIconAlertCircle as Icon;
+const IconAlertTriangle = RawIconAlertTriangle as Icon;
+const IconBolt = RawIconBolt as Icon;
+const IconBriefcase = RawIconBriefcase as Icon;
+const IconBroadcast = RawIconBroadcast as Icon;
+const IconCheck = RawIconCheck as Icon;
+const IconChevronDown = RawIconChevronDown as Icon;
+const IconClipboard = RawIconClipboard as Icon;
+const IconCloud = RawIconCloud as Icon;
+const IconCode = RawIconCode as Icon;
+const IconCopy = RawIconCopy as Icon;
+const IconCpu = RawIconCpu as Icon;
+const IconCrown = RawIconCrown as Icon;
+const IconDeviceDesktop = RawIconDeviceDesktop as Icon;
+const IconDeviceDesktopCog = RawIconDeviceDesktopCog as Icon;
+const IconDots = RawIconDots as Icon;
+const IconDownload = RawIconDownload as Icon;
+const IconExternalLink = RawIconExternalLink as Icon;
+const IconFileMusic = RawIconFileMusic as Icon;
+const IconGauge = RawIconGauge as Icon;
+const IconHeadphones = RawIconHeadphones as Icon;
+const IconHelpCircle = RawIconHelpCircle as Icon;
+const IconHome = RawIconHome as Icon;
+const IconInfoCircle = RawIconInfoCircle as Icon;
+const IconKeyboard = RawIconKeyboard as Icon;
+const IconLanguage = RawIconLanguage as Icon;
+const IconListCheck = RawIconListCheck as Icon;
+const IconLoader2 = RawIconLoader2 as Icon;
+const IconLogout = RawIconLogout as Icon;
+const IconMail = RawIconMail as Icon;
+const IconMessage = RawIconMessage as Icon;
+const IconMicrophone = RawIconMicrophone as Icon;
+const IconMoon = RawIconMoon as Icon;
+const IconPencil = RawIconPencil as Icon;
+const IconPhoneCall = RawIconPhoneCall as Icon;
+const IconPlayerPlay = RawIconPlayerPlay as Icon;
+const IconPlus = RawIconPlus as Icon;
+const IconRefresh = RawIconRefresh as Icon;
+const IconRotate2 = RawIconRotate2 as Icon;
+const IconSearch = RawIconSearch as Icon;
+const IconServer = RawIconServer as Icon;
+const IconSparkles = RawIconSparkles as Icon;
+const IconSpeakerphone = RawIconSpeakerphone as Icon;
+const IconSquare = RawIconSquare as Icon;
+const IconSun = RawIconSun as Icon;
+const IconTargetArrow = RawIconTargetArrow as Icon;
+const IconTrash = RawIconTrash as Icon;
+const IconTypography = RawIconTypography as Icon;
+const IconUser = RawIconUser as Icon;
+const IconVolume = RawIconVolume as Icon;
+const IconX = RawIconX as Icon;
+
+export {
+ IconAdjustmentsHorizontal,
+ IconAlertCircle,
+ IconAlertTriangle,
+ IconBolt,
+ IconBriefcase,
+ IconBroadcast,
+ IconCheck,
+ IconChevronDown,
+ IconClipboard,
+ IconCloud,
+ IconCode,
+ IconCopy,
+ IconCpu,
+ IconCrown,
+ IconDeviceDesktop,
+ IconDeviceDesktopCog,
+ IconDots,
+ IconDownload,
+ IconExternalLink,
+ IconFileMusic,
+ IconGauge,
+ IconHeadphones,
+ IconHelpCircle,
+ IconHome,
+ IconInfoCircle,
+ IconKeyboard,
+ IconLanguage,
+ IconListCheck,
+ IconLoader2,
+ IconLogout,
+ IconMail,
+ IconMessage,
+ IconMicrophone,
+ IconMoon,
+ IconPencil,
+ IconPhoneCall,
+ IconPlayerPlay,
+ IconPlus,
+ IconRefresh,
+ IconRotate2,
+ IconSearch,
+ IconServer,
+ IconSparkles,
+ IconSpeakerphone,
+ IconSquare,
+ IconSun,
+ IconTargetArrow,
+ IconTrash,
+ IconTypography,
+ IconUser,
+ IconVolume,
+ IconX,
+};
diff --git a/src/windows/settings/SettingsApp.tsx b/src/windows/settings/SettingsApp.tsx
index 590ac9a..9dbe480 100644
--- a/src/windows/settings/SettingsApp.tsx
+++ b/src/windows/settings/SettingsApp.tsx
@@ -4,17 +4,17 @@ import { getVersion } from "@tauri-apps/api/app";
import { listen } from "@tauri-apps/api/event";
import { openUrl } from "@tauri-apps/plugin-opener";
import {
- Download,
- ExternalLink,
- FileAudio,
- Home,
- Cpu,
- Languages,
- Loader2,
- Sparkles,
- Sliders,
- LucideIcon,
-} from "lucide-react";
+ IconDownload,
+ IconExternalLink,
+ IconFileMusic,
+ IconHome,
+ IconCpu,
+ IconLanguage,
+ IconLoader2,
+ IconSparkles,
+ IconAdjustmentsHorizontal,
+ Icon,
+} from "../../lib/icons";
import { TitleBar } from "../../components/TitleBar";
import { MainTab } from "./tabs/MainTab";
import { FileTranscriptionTab } from "./tabs/FileTranscriptionTab";
@@ -72,31 +72,31 @@ function resolveInitialTab(): Tab {
return "main";
}
-const TABS: { id: Tab; labelKey: MsgKey; icon: LucideIcon; note: string }[] = [
- { id: "main", labelKey: "settingsApp.tab.main", icon: Home, note: "История записей" },
+const TABS: { id: Tab; labelKey: MsgKey; icon: Icon; note: string }[] = [
+ { id: "main", labelKey: "settingsApp.tab.main", icon: IconHome, note: "История записей" },
{
id: "file",
labelKey: "settingsApp.tab.file",
- icon: FileAudio,
+ icon: IconFileMusic,
note: "Транскрибация",
},
{
id: "interpreter",
labelKey: "settingsApp.tab.interpreter",
- icon: Languages,
+ icon: IconLanguage,
note: "Realtime Interpreter beta",
},
{
id: "model",
labelKey: "settingsApp.tab.model",
- icon: Cpu,
+ icon: IconCpu,
note: "Ключи и подключение модели",
},
- { id: "style", labelKey: "settingsApp.tab.style", icon: Sparkles, note: "Стиль обработки и Промпты для саммари" },
+ { id: "style", labelKey: "settingsApp.tab.style", icon: IconSparkles, note: "Стиль обработки и Промпты для саммари" },
{
id: "settings",
labelKey: "settingsApp.tab.settings",
- icon: Sliders,
+ icon: IconAdjustmentsHorizontal,
note: "Язык, микрофон и горячая клавиша",
},
];
@@ -119,7 +119,7 @@ function TabButton({
className={`nav-item ${isActive ? "active" : ""}`}
style={{ width: "100%", textAlign: "left", font: "inherit" }}
>
-
+
{t(tab.labelKey)}
);
@@ -267,16 +267,16 @@ function AppUpdateFooter(): ReactElement | null {
}}
>
{installing ? (
-
) : (
-
+
)}
{installing
@@ -329,9 +329,9 @@ function AppUpdateFooter(): ReactElement | null {
v{version}
-
@@ -377,13 +377,21 @@ export function SettingsApp() {
useEffect(() => {
Promise.all([getPermissionsPassed(), checkAllPermissions(), getHistory()])
- .then(([passed, permissions, history]) => {
- const hasRequiredPermissions =
+ .then(async ([passed, permissions, history]) => {
+ const hasRequiredStartupPermissions =
permissions.microphone !== "denied" &&
- permissions.accessibility === "granted" &&
- (!isMacPlatform() || permissions.systemAudio === "granted");
+ (!isMacPlatform() || permissions.accessibility === "granted");
+ const shouldRecoverExistingInstall =
+ !passed && history.length > 0 && hasRequiredStartupPermissions;
+
+ if (shouldRecoverExistingInstall) {
+ await setPermissionsPassed(true);
+ }
+
setInitialHistory(history);
- setShowPermissions(!(passed && hasRequiredPermissions));
+ setShowPermissions(
+ !((passed || shouldRecoverExistingInstall) && hasRequiredStartupPermissions),
+ );
setLoadError(null);
})
.catch((error) => {
diff --git a/src/windows/settings/tabs/FileTranscriptionTab.tsx b/src/windows/settings/tabs/FileTranscriptionTab.tsx
index a67bcac..6e00e99 100644
--- a/src/windows/settings/tabs/FileTranscriptionTab.tsx
+++ b/src/windows/settings/tabs/FileTranscriptionTab.tsx
@@ -5,14 +5,14 @@ import { emit, listen } from "@tauri-apps/api/event";
import { getCurrentWebview } from "@tauri-apps/api/webview";
import { open } from "@tauri-apps/plugin-dialog";
import {
- AlertCircle,
- Check,
- Clipboard,
- FileAudio,
- ListChecks,
- Loader2,
- X,
-} from "lucide-react";
+ IconAlertCircle,
+ IconCheck,
+ IconClipboard,
+ IconFileMusic,
+ IconListCheck,
+ IconLoader2,
+ IconX,
+} from "../../../lib/icons";
import {
addHistoryEntry,
@@ -585,7 +585,7 @@ export function FileTranscriptionTab({
if (speakerMode && !settings.useOwnKey && !cloudSpeakerReady) {
setSpeakerDiarization(false);
await saveSettings({ fileSpeakerDiarization: false });
- throw new Error("Cloud speaker diarization unavailable");
+ throw new Error("IconCloud speaker diarization unavailable");
}
if (speakerMode && settings.useOwnKey && !isSpeakerSetupReady(settings)) {
@@ -945,7 +945,7 @@ export function FileTranscriptionTab({
setSpeakerSetupIntent(null);
setError(
toFileTranscriptionErrorMessage(
- new Error("Cloud speaker diarization unavailable"),
+ new Error("IconCloud speaker diarization unavailable"),
),
);
setSpeakerDiarization(false);
@@ -1092,13 +1092,13 @@ export function FileTranscriptionTab({
}}
>
{isProcessing ? (
-
) : (
-
+
)}
@@ -1166,9 +1166,9 @@ export function FileTranscriptionTab({
textAlign: "left",
}}
>
-
{error}
@@ -1271,9 +1271,9 @@ export function FileTranscriptionTab({
style={{ minHeight: 32, padding: "0 12px" }}
>
{copied ? (
-
+
) : (
-
+
)}
{copied ? t("fileTab.result.copied") : t("fileTab.result.copy")}
@@ -1288,7 +1288,7 @@ export function FileTranscriptionTab({
title={summaryAvailable ? undefined : t("summary.unavailable.tooltip")}
style={{ minHeight: 32, padding: "0 12px", gap: 6, opacity: summaryAvailable ? 1 : 0.5, cursor: summaryAvailable ? "pointer" : "not-allowed" }}
>
-
+
{t("summary.button")}
)}
@@ -1305,7 +1305,7 @@ export function FileTranscriptionTab({
style={{ width: 32, minWidth: 32, minHeight: 32, padding: 0 }}
title={t("fileTab.result.clear")}
>
-
+
)}
@@ -1322,7 +1322,7 @@ export function FileTranscriptionTab({
color: "var(--text-mid)",
}}
>
-
+
{t("summary.unavailable.note")}
)}
@@ -1573,9 +1573,9 @@ export function FileTranscriptionTab({
}}
>
{localWhisperDownloaded ? (
-
+
) : (
-
+
)}
{localWhisperDownloaded
@@ -1594,9 +1594,9 @@ export function FileTranscriptionTab({
}}
>
{diarizationInstalled ? (
-
+
) : (
-
+
)}
{diarizationInstalled
@@ -1675,10 +1675,10 @@ export function FileTranscriptionTab({
}}
>
{speakerSetupInstalling ? (
-
) : null}
{speakerSetupActionLabel}
diff --git a/src/windows/settings/tabs/MainTab.tsx b/src/windows/settings/tabs/MainTab.tsx
index f6dcd05..1741dcf 100644
--- a/src/windows/settings/tabs/MainTab.tsx
+++ b/src/windows/settings/tabs/MainTab.tsx
@@ -19,17 +19,17 @@ import {
type SpeakerTranscriptSegment,
} from "../../../lib/store";
import {
- AlertCircle,
- Check,
- Copy,
- HelpCircle,
- Loader2,
- ListChecks,
- Pencil,
- RotateCcw,
- Square,
- Trash2,
-} from "lucide-react";
+ IconAlertCircle,
+ IconCheck,
+ IconCopy,
+ IconHelpCircle,
+ IconLoader2,
+ IconListCheck,
+ IconPencil,
+ IconRotate2,
+ IconSquare,
+ IconTrash,
+} from "../../../lib/icons";
import {
HISTORY_CLEARED_EVENT,
HISTORY_DELETED_EVENT,
@@ -737,7 +737,7 @@ export function MainTab({ initialHistory = [] }: MainTabProps) {
color: hintHelpOpen ? "var(--text-hi)" : "var(--text-low)",
}}
>
-
+
)}
@@ -854,7 +854,7 @@ export function MainTab({ initialHistory = [] }: MainTabProps) {
: t("mainTab.clearAllTitle")
}
>
- {" "}
+ {" "}
{isClearArmed ? t("mainTab.confirm") : t("mainTab.clear")}
)}
@@ -1087,10 +1087,10 @@ export function MainTab({ initialHistory = [] }: MainTabProps) {
width: "fit-content",
}}
>
-
{t("mainTab.processing")}
@@ -1113,9 +1113,9 @@ export function MainTab({ initialHistory = [] }: MainTabProps) {
width: "fit-content",
}}
>
-
{item.status === "interrupted"
@@ -1180,9 +1180,9 @@ export function MainTab({ initialHistory = [] }: MainTabProps) {
}}
title={t("mainTab.stopProcessing")}
>
-
@@ -1210,13 +1210,13 @@ export function MainTab({ initialHistory = [] }: MainTabProps) {
title={t("mainTab.retryProcess")}
>
{retryingId === item.id ? (
-
) : (
-
+
)}
) : (
@@ -1228,7 +1228,7 @@ export function MainTab({ initialHistory = [] }: MainTabProps) {
key: "edit",
label: t("rowMenu.edit"),
icon: (
-
+
),
onSelect: () => editEntry(item),
},
@@ -1242,12 +1242,12 @@ export function MainTab({ initialHistory = [] }: MainTabProps) {
icon:
copied === item.id ||
retrySucceededId === item.id ? (
-
) : (
-
+
),
onSelect: () =>
copyText(item.id, item.cleaned),
@@ -1256,9 +1256,9 @@ export function MainTab({ initialHistory = [] }: MainTabProps) {
key: "summarize",
label: t("rowMenu.summarize"),
icon: (
-
),
disabled: !summaryAvailable,
@@ -1283,7 +1283,7 @@ export function MainTab({ initialHistory = [] }: MainTabProps) {
}}
title={t("mainTab.delete")}
>
-
+
)}
diff --git a/src/windows/settings/tabs/RealtimeInterpreterTab.tsx b/src/windows/settings/tabs/RealtimeInterpreterTab.tsx
index 242481c..5c3e7d7 100644
--- a/src/windows/settings/tabs/RealtimeInterpreterTab.tsx
+++ b/src/windows/settings/tabs/RealtimeInterpreterTab.tsx
@@ -2,17 +2,17 @@ import { useEffect, useMemo, useState } from "react";
import type { CSSProperties, ReactElement } from "react";
import { emit, listen } from "@tauri-apps/api/event";
import {
- AlertTriangle,
- Headphones,
- Languages,
- Mic,
- Play,
- Radio,
- RefreshCcw,
- Speaker,
- Square,
- Volume2,
-} from "lucide-react";
+ IconAlertTriangle,
+ IconHeadphones,
+ IconLanguage,
+ IconMicrophone,
+ IconPlayerPlay,
+ IconBroadcast,
+ IconRefresh,
+ IconSpeakerphone,
+ IconSquare,
+ IconVolume,
+} from "../../../lib/icons";
import {
addHistoryEntry,
@@ -691,7 +691,7 @@ export function RealtimeInterpreterTab(): ReactElement {
className="label"
style={{ display: "inline-flex", alignItems: "center", gap: 8 }}
>
-
+
Beta
-
+
@@ -733,7 +733,7 @@ export function RealtimeInterpreterTab(): ReactElement {
fontWeight: 700,
}}
>
-
+
{status?.message || t("realtime.statusMessage.off")}
@@ -831,7 +831,7 @@ export function RealtimeInterpreterTab(): ReactElement {
background: "var(--control-muted)",
}}
>
- Cloud proxy
+ IconCloud proxy
-
+
{t("realtime.headphonesConfirm")}
@@ -884,7 +884,7 @@ export function RealtimeInterpreterTab(): ReactElement {
}}
>
-
+
@@ -903,7 +903,7 @@ export function RealtimeInterpreterTab(): ReactElement {
}}
style={{ minHeight: 36, borderRadius: 10, padding: "0 12px" }}
>
-
+
{t("realtime.button.refresh")}
{status?.active ? (
@@ -934,7 +934,7 @@ export function RealtimeInterpreterTab(): ReactElement {
}}
style={{ minHeight: 36, borderRadius: 10, padding: "0 12px" }}
>
-
+
{t("realtime.button.stop")}
) : (
@@ -948,7 +948,7 @@ export function RealtimeInterpreterTab(): ReactElement {
}}
style={{ minHeight: 36, borderRadius: 10, padding: "0 12px" }}
>
-
+
{t("realtime.button.start")}
)}
@@ -973,9 +973,9 @@ export function RealtimeInterpreterTab(): ReactElement {
lineHeight: 1.5,
}}
>
-
{error}
@@ -1026,7 +1026,7 @@ export function RealtimeInterpreterTab(): ReactElement {
>
-
+
{t("realtime.panel.youToCall")}
-
+
{t("realtime.panel.callToYou")}
= [
- { id: "system", Icon: Monitor },
- { id: "light", Icon: Sun },
- { id: "dark", Icon: Moon },
+const THEME_OPTIONS: Array<{ id: AppSettings["theme"]; Icon: Icon }> = [
+ { id: "system", Icon: IconDeviceDesktop },
+ { id: "light", Icon: IconSun },
+ { id: "dark", Icon: IconMoon },
];
export function SettingsTab() {
@@ -340,7 +340,7 @@ export function SettingsTab() {
setMicStatus("ready");
setMicMessage(t("settingsGeneralExtra.mic.inUse", { label: activeLabel }));
} catch (err) {
- void logError("SETTINGS", `Mic enumeration error: ${err instanceof Error ? err.message : String(err)}`);
+ void logError("SETTINGS", `IconMicrophone enumeration error: ${err instanceof Error ? err.message : String(err)}`);
setMicStatus("empty");
setMicMessage(t("settingsGeneralExtra.mic.enumFailed"));
}
@@ -734,7 +734,7 @@ export function SettingsTab() {
gap: 4,
}}
>
-
+
{t(`settings.theme.${id}`)}
);
@@ -754,12 +754,12 @@ export function SettingsTab() {
{currentLang ? `${currentLang.native} (${currentLang.name})` : settings.language}
-
+
{langOpen && (
-
+
setLangSearch(e.target.value)} placeholder={t("settings.recognitionLang.searchPlaceholder")} style={{ border: "none", outline: "none", background: "transparent", fontSize: 12, color: "var(--text-hi)", flex: 1 }} />
@@ -789,7 +789,7 @@ export function SettingsTab() {
{lang.code}
{lang.native}
{lang.name}
- {settings.language === lang.code && }
+ {settings.language === lang.code && }
))}
@@ -817,7 +817,7 @@ export function SettingsTab() {
{microphones.length === 0 ? t("settings.mic.systemDefault") : visibleMicrophoneLabel}
-
+
{micOpen && microphones.length > 0 && (
@@ -830,7 +830,7 @@ export function SettingsTab() {
onMouseLeave={(e) => e.currentTarget.style.background = settings.micId === "" ? "var(--dropdown-active)" : "transparent"}
>
{t("settings.mic.systemDefault")}
- {settings.micId === "" &&
}
+ {settings.micId === "" &&
}
{microphones.map((m, i) => (
))}
@@ -1103,7 +1103,7 @@ export function SettingsTab() {
className="btn"
style={{ minHeight: CONTROL_HEIGHT, width: "100%", justifySelf: "end", justifyContent: "center", gap: 8, padding: "0 10px", borderRadius: CONTROL_RADIUS, fontSize: CONTROL_FONT_SIZE }}
>
-
+
{t("settings.support.button")}
diff --git a/src/windows/settings/tabs/SettingsTabs.tsx b/src/windows/settings/tabs/SettingsTabs.tsx
index 9b2e128..34b0bbd 100644
--- a/src/windows/settings/tabs/SettingsTabs.tsx
+++ b/src/windows/settings/tabs/SettingsTabs.tsx
@@ -5,29 +5,29 @@ import { emit } from "@tauri-apps/api/event";
import { onOpenUrl } from "@tauri-apps/plugin-deep-link";
import { openUrl } from "@tauri-apps/plugin-opener";
import {
- Briefcase,
- Check,
- Cloud,
- Code,
- Crown,
- Download,
- Gauge,
- HardDrive,
- LogOut,
- LucideIcon,
- MessageSquare,
- Mic,
- Pencil,
- Plus,
- Server,
- Sparkles,
- Target,
- Trash2,
- Type,
- User,
- X,
- Zap,
-} from "lucide-react";
+ IconBriefcase,
+ IconCheck,
+ IconCloud,
+ IconCode,
+ IconCrown,
+ IconDownload,
+ IconGauge,
+ IconDeviceDesktopCog,
+ IconLogout,
+ Icon,
+ IconMessage,
+ IconMicrophone,
+ IconPencil,
+ IconPlus,
+ IconServer,
+ IconSparkles,
+ IconTargetArrow,
+ IconTrash,
+ IconTypography,
+ IconUser,
+ IconX,
+ IconBolt,
+} from "../../../lib/icons";
import {
AppSettings,
@@ -605,7 +605,7 @@ function OptionCard({ active = false, icon, title, description, badge, onClick,
{active && (
-
+
)}
@@ -645,7 +645,7 @@ function CloudSubscriptionAccountCard({
style={{ width: "100%", height: "100%", borderRadius: "50%", objectFit: "cover" }}
/>
) : (
-
+
)}
@@ -692,7 +692,7 @@ function CloudSubscriptionAccountCard({
}}
title={t("models.account.logout")}
>
-
+
@@ -706,7 +706,7 @@ function SubscriptionPromoContent({ onActivate }: { onActivate: () => void }) {
return (
<>
-
+
{t("models.guest.title")}
{t("models.cta.freeTrial")}
@@ -733,7 +733,7 @@ function SubscriptionGuestCard({ onActivate }: { onActivate: () => void }) {
}
/**
- * Three-state subscription block, shared by the Models → Cloud section and the
+ * Three-state subscription block, shared by the Models → IconCloud section and the
* dedicated "Подписка Talkis" tab: active-subscription banner, signed-in account
* card (activate / log out), or guest promo (sign in + start the free trial).
*/
@@ -753,7 +753,7 @@ function SubscriptionCards({
-
+
{t("models.subscription.active")}
@@ -1032,7 +1032,7 @@ function PromptLibrary({
}
+ icon={
}
title={preset.name}
description={
}
onClick={() => update({ defaultSummaryPromptId: preset.id })}
@@ -1067,7 +1067,7 @@ function PromptLibrary({
fontFamily: "var(--font-main)",
}}
>
-
+
{t("models.prompt.create")}
@@ -1090,7 +1090,7 @@ function PromptLibrary({
fontFamily: "var(--font-main)",
}}
>
-
+
{t("models.common.edit")}
>
@@ -1153,7 +1153,7 @@ function TextModelCard({
style={{ width: "100%", border: "none", background: "transparent", padding: "12px 14px", display: "flex", alignItems: "center", gap: 12, cursor: "pointer", textAlign: "left", fontFamily: "var(--font-main)" }}
>
-
+
@@ -1356,7 +1356,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
void syncSettings().catch(() => {});
}, [syncSettings]);
- // Cloud profile — always fetch (regardless of tab) so hooks are stable
+ // IconCloud profile — always fetch (regardless of tab) so hooks are stable
useEffect(() => {
if (getCachedCloudProfile() === undefined) {
loadCloudProfile().catch(() => {});
@@ -1602,7 +1602,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
});
};
- // Subscription activation / logout — shared by the Models → Cloud section and
+ // Subscription activation / logout — shared by the Models → IconCloud section and
// the dedicated "Подписка Talkis" tab so both surfaces drive the same flow.
const handleActivateSubscription = async () => {
try {
@@ -1657,22 +1657,22 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
const modeOptions: Array<{
id: ModelMode;
label: string;
- Icon: LucideIcon;
+ Icon: Icon;
}> = [
{
id: "cloud",
label: t("models.mode.cloud"),
- Icon: Cloud,
+ Icon: IconCloud,
},
{
id: "api",
label: "API",
- Icon: Code,
+ Icon: IconCode,
},
{
id: "local",
label: t("models.mode.local"),
- Icon: Server,
+ Icon: IconServer,
},
];
@@ -2054,7 +2054,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
return (
- {isActive && }
+ {isActive && }
{isActive ? t("models.modeCommit.active") : t("models.modeCommit.label", { mode: modeLabel })}
{!isActive && (
@@ -2252,9 +2252,9 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
const renderLocalModelStats = (model: LocalModelOption) => {
const speedValueLabel = translateSpeedValue(model.speed);
const accuracyValueLabel = translateAccuracyValue(model.accuracy);
- const stats: { key: string; title: string; label: string; Icon: LucideIcon; level: number }[] = [
- { key: "speed", title: t("models.stat.speedTitle", { value: speedValueLabel }), label: t("models.stat.speed"), Icon: Gauge, level: getLocalModelLevel("speed", model.speed) },
- { key: "accuracy", title: t("models.stat.accuracyTitle", { value: accuracyValueLabel }), label: t("models.stat.accuracy"), Icon: Target, level: getLocalModelLevel("accuracy", model.accuracy) },
+ const stats: { key: string; title: string; label: string; Icon: Icon; level: number }[] = [
+ { key: "speed", title: t("models.stat.speedTitle", { value: speedValueLabel }), label: t("models.stat.speed"), Icon: IconGauge, level: getLocalModelLevel("speed", model.speed) },
+ { key: "accuracy", title: t("models.stat.accuracyTitle", { value: accuracyValueLabel }), label: t("models.stat.accuracy"), Icon: IconTargetArrow, level: getLocalModelLevel("accuracy", model.accuracy) },
];
const storageLabel = getLocalModelStorageLabel(model);
@@ -2265,7 +2265,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
aria-label={t("models.stat.downloadSizeTitle", { value: storageLabel })}
style={{ display: "flex", alignItems: "center", gap: 6 }}
>
-
+
{storageLabel}
@@ -2278,7 +2278,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
aria-label={title}
style={{ display: "flex", alignItems: "center", gap: 6 }}
>
-
+
{label}
@@ -2356,7 +2356,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
if (message.includes("отменена")) {
- // User cancelled — reset to the not-downloaded state silently.
+ // IconUser cancelled — reset to the not-downloaded state silently.
setLocalModelActionStates((prev) => {
const next = { ...prev };
delete next[model.id];
@@ -2474,7 +2474,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
gap: 7,
}}
>
-
+
{label}
);
@@ -2504,7 +2504,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
{(isCloudSelected || hasActiveSubscription) && (
-
+
{isCloudSelected ? t("models.connection.usedForRecognition") : t("models.cloud.proReady")}
@@ -2521,7 +2521,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
alignItems: "center",
gap: 8,
}}>
-
+
{t("models.common.selected")}
) : hasActiveSubscription ? (
@@ -2543,7 +2543,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
gap: 8,
}}
>
-
+
{t("models.common.select")}
) : null}
@@ -2558,8 +2558,8 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
{([
- { id: "transcription", label: t("models.local.tabTranscription"), Icon: Mic },
- { id: "text", label: t("models.local.tabText"), Icon: MessageSquare },
+ { id: "transcription", label: t("models.local.tabTranscription"), Icon: IconMicrophone },
+ { id: "text", label: t("models.local.tabText"), Icon: IconMessage },
] as const).map(({ id, label, Icon }) => {
const active = apiModelKind === id;
@@ -2585,7 +2585,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
gap: 7,
}}
>
-
+
{label}
{id === "text" && !apiTextModelConfigured && (
- {adapterStatus.status === "success" && }
+ {adapterStatus.status === "success" && }
{adapterStatus.connectionLabel}
{isAdapterSelected ? (
@@ -2760,7 +2760,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
alignItems: "center",
gap: 8,
}}>
-
+
{t("models.common.selected")}
) : (
@@ -2784,7 +2784,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
gap: 8,
}}
>
-
+
{t("models.common.select")}
)}
@@ -2814,7 +2814,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
>
) : (
<>
-
+
{adapter.testable ? t("models.test.testAndSave") : t("models.test.saveButton")}
>
)}
@@ -2839,8 +2839,8 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
{([
- { id: "transcription", label: t("models.local.tabTranscription"), Icon: Mic },
- { id: "text", label: t("models.local.tabText"), Icon: MessageSquare },
+ { id: "transcription", label: t("models.local.tabTranscription"), Icon: IconMicrophone },
+ { id: "text", label: t("models.local.tabText"), Icon: IconMessage },
] as const).map(({ id, label, Icon }) => {
const active = localModelKind === id;
@@ -2866,7 +2866,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
gap: 7,
}}
>
-
+
{label}
{id === "text" && !localTextModelSelected && (
{modelStatus.connectionLabel && (
- {(modelStatus.status === "installed" || modelStatus.status === "selected") && }
+ {(modelStatus.status === "installed" || modelStatus.status === "selected") && }
{modelStatus.connectionLabel}
)}
@@ -3029,7 +3029,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
gap: 8,
}}
>
-
+
{t("models.common.select")}
)}
@@ -3062,12 +3062,12 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
>
{isDownloaded ? (
<>
-
+
{t("models.common.delete")}
>
) : (
<>
-
+
{isRuntimeReady ? t("models.common.download") : t("models.common.unavailable")}
>
)}
@@ -3092,7 +3092,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
gap: 8,
}}
>
-
+
{t("models.common.cancel")}
)}
@@ -3189,19 +3189,19 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
);
}
- const STYLE_ICONS: Record = {
- classic: MessageSquare,
- business: Briefcase,
- tech: Code,
+ const STYLE_ICONS: Record = {
+ classic: IconMessage,
+ business: IconBriefcase,
+ tech: IconCode,
};
const styleTabOptions: Array<{
id: "style" | "prompts";
label: string;
- Icon: LucideIcon;
+ Icon: Icon;
}> = [
- { id: "style", label: t("models.styleTab.style"), Icon: Type },
- { id: "prompts", label: t("models.styleTab.prompts"), Icon: MessageSquare },
+ { id: "style", label: t("models.styleTab.style"), Icon: IconTypography },
+ { id: "prompts", label: t("models.styleTab.prompts"), Icon: IconMessage },
];
return (
@@ -3239,7 +3239,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
gap: 7,
}}
>
-
+
{label}
);
@@ -3256,7 +3256,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
}
+ icon={}
title={st.title}
description={st.description}
onClick={() =>
diff --git a/src/windows/widget/Widget.tsx b/src/windows/widget/Widget.tsx
index ce6cb8c..ebac953 100644
--- a/src/windows/widget/Widget.tsx
+++ b/src/windows/widget/Widget.tsx
@@ -5,7 +5,7 @@ import { listen } from "@tauri-apps/api/event";
import { getCurrentWebview } from "@tauri-apps/api/webview";
import { cursorPosition, getCurrentWindow } from "@tauri-apps/api/window";
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
-import { Check, Copy, FileAudio, Loader2, PhoneCall } from "lucide-react";
+import { IconCheck, IconCopy, IconFileMusic, IconLoader2, IconPhoneCall } from "../../lib/icons";
import {
HISTORY_CLEARED_EVENT,
@@ -1292,15 +1292,15 @@ function FileDropPill({
}}
>
{isProcessing ? (
-
) : isSuccess ? (
-
+
) : (
-
+
)}
{isStarting || isProcessing ? (
-
) : isSuccess ? (
-
+
) : isError ? (
) : (
-
+
)}
@@ -1627,9 +1627,9 @@ function IdlePill({
}}
>
{copySucceeded ? (
-
+
) : (
-
+
)}
)}
@@ -1783,8 +1783,8 @@ function FlowRecordingWidget({
)}
{state === "long" && (
- {longMark === "phone" && }
- {longMark === "success" && }
+ {longMark === "phone" && }
+ {longMark === "success" && }
{longMark === "error" && "!"}
)}
diff --git a/src/windows/widget/WidgetNoticeOverlay.tsx b/src/windows/widget/WidgetNoticeOverlay.tsx
index 2834c7d..3175e2e 100644
--- a/src/windows/widget/WidgetNoticeOverlay.tsx
+++ b/src/windows/widget/WidgetNoticeOverlay.tsx
@@ -2,7 +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 { IconAlertCircle, IconInfoCircle } from "../../lib/icons";
import { NOTICE_AREA_HEIGHT, NOTICE_WIDGET_WIDTH, WIDGET_NOTICE_EVENT, type WidgetNoticeState } from "./widgetConstants";
@@ -50,7 +50,7 @@ export function WidgetNoticeOverlay(): ReactElement | null {
const toggleExpanded = () => {
setExpanded((value) => !value);
};
- const Icon = notice.tone === "error" ? AlertCircle : Info;
+ const Icon = notice.tone === "error" ? IconAlertCircle : IconInfoCircle;
const iconColor = notice.tone === "error" ? "rgba(184,52,52,0.9)" : "rgba(0,0,0,0.58)";
return (
@@ -113,7 +113,7 @@ export function WidgetNoticeOverlay(): ReactElement | null {
>