Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,12 @@ docs/plans/
*.spec
build_error_log.txt


# Auto Claude generated files
.auto-claude/
.auto-claude-security.json
.auto-claude-status
.claude_settings.json
.worktrees/
.security-key
logs/security/
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
"dev": "npm-run-all --parallel vite pyloid",
"dev:watch": "npm-run-all --parallel vite pyloid:watch",
"vite": "vite",
"pyloid": "uv run -p .venv ./src-pyloid/main.py",
"pyloid": "uv run ./src-pyloid/main.py",
"pyloid:watch": "uv run pyloid-watcher --path ./src-pyloid --pattern \"*.py\" --command \"uv run ./src-pyloid/main.py\"",
"build": "tsc -b && vite build && uv run -p .venv ./src-pyloid/build/build.py",
"build": "tsc -b && vite build && uv run ./src-pyloid/build/build.py",
"build:installer": "pnpm run build && cd installer && build-installer.bat",
"setup": "pnpm install && uv venv && uv sync"
},
Expand Down
38 changes: 22 additions & 16 deletions src-pyloid/app_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,22 +178,25 @@ def transcribe():
info("Pasting text at cursor...")
self.clipboard_service.paste_at_cursor(text)

# Save to history (and audio if enabled)
history_id = self.db.add_history(text)

if settings.save_audio_to_history:
try:
audio_meta = self._save_audio_attachment(history_id, audio)
self.db.update_history_audio(
history_id,
audio_relpath=audio_meta["audio_relpath"],
audio_duration_ms=audio_meta["audio_duration_ms"],
audio_size_bytes=audio_meta["audio_size_bytes"],
audio_mime=audio_meta["audio_mime"],
)
info(f"Saved audio attachment for history {history_id}")
except (OSError, wave.Error, sqlite3.Error, ValueError) as exc:
warning(f"Failed to save audio attachment: {exc}")
# Save to history (and audio if enabled) - only if not disabled
if not settings.disable_history_storage:
history_id = self.db.add_history(text)

if settings.save_audio_to_history:
try:
audio_meta = self._save_audio_attachment(history_id, audio)
self.db.update_history_audio(
history_id,
audio_relpath=audio_meta["audio_relpath"],
audio_duration_ms=audio_meta["audio_duration_ms"],
audio_size_bytes=audio_meta["audio_size_bytes"],
audio_mime=audio_meta["audio_mime"],
)
info(f"Saved audio attachment for history {history_id}")
except (OSError, wave.Error, sqlite3.Error, ValueError) as exc:
warning(f"Failed to save audio attachment: {exc}")
else:
info("History storage disabled - transcription not saved")

if self._on_transcription_complete:
self._on_transcription_complete(text)
Expand Down Expand Up @@ -230,6 +233,7 @@ def get_settings(self) -> dict:
"onboardingComplete": settings.onboarding_complete,
"microphone": settings.microphone,
"saveAudioToHistory": settings.save_audio_to_history,
"disableHistoryStorage": settings.disable_history_storage,
"holdHotkey": settings.hold_hotkey,
"holdHotkeyEnabled": settings.hold_hotkey_enabled,
"toggleHotkey": settings.toggle_hotkey,
Expand All @@ -246,6 +250,8 @@ def update_settings(self, **kwargs) -> dict:
mapped["onboarding_complete"] = kwargs["onboardingComplete"]
if "saveAudioToHistory" in kwargs:
mapped["save_audio_to_history"] = kwargs["saveAudioToHistory"]
if "disableHistoryStorage" in kwargs:
mapped["disable_history_storage"] = kwargs["disableHistoryStorage"]
# Hotkey settings (camelCase to snake_case)
if "holdHotkey" in kwargs:
mapped["hold_hotkey"] = kwargs["holdHotkey"]
Expand Down
3 changes: 3 additions & 0 deletions src-pyloid/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ async def update_settings(
holdHotkeyEnabled: Optional[bool] = None,
toggleHotkey: Optional[str] = None,
toggleHotkeyEnabled: Optional[bool] = None,
disableHistoryStorage: Optional[bool] = None,
):
controller = get_controller()
kwargs = {}
Expand Down Expand Up @@ -90,6 +91,8 @@ async def update_settings(
kwargs["toggleHotkey"] = toggleHotkey
if toggleHotkeyEnabled is not None:
kwargs["toggleHotkeyEnabled"] = toggleHotkeyEnabled
if disableHistoryStorage is not None:
kwargs["disableHistoryStorage"] = disableHistoryStorage

# Check if onboarding was already complete before this update
old_settings = controller.get_settings()
Expand Down
5 changes: 5 additions & 0 deletions src-pyloid/services/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class Settings:
onboarding_complete: bool = False
microphone: int = -1 # -1 = default device, otherwise device id
save_audio_to_history: bool = False
disable_history_storage: bool = False
# Hotkey settings
hold_hotkey: str = "ctrl+win"
hold_hotkey_enabled: bool = True
Expand All @@ -74,6 +75,7 @@ def get_settings(self) -> Settings:
onboarding_complete=self.db.get_setting("onboarding_complete", "false") == "true",
microphone=int(self.db.get_setting("microphone", "-1")),
save_audio_to_history=self.db.get_setting("save_audio_to_history", "false") == "true",
disable_history_storage=self.db.get_setting("disable_history_storage", "false") == "true",
# Hotkey settings
hold_hotkey=self.db.get_setting("hold_hotkey", "ctrl+win"),
hold_hotkey_enabled=self.db.get_setting("hold_hotkey_enabled", "true") == "true",
Expand All @@ -95,6 +97,7 @@ def update_settings(
onboarding_complete: Optional[bool] = None,
microphone: Optional[int] = None,
save_audio_to_history: Optional[bool] = None,
disable_history_storage: Optional[bool] = None,
hold_hotkey: Optional[str] = None,
hold_hotkey_enabled: Optional[bool] = None,
toggle_hotkey: Optional[str] = None,
Expand All @@ -118,6 +121,8 @@ def update_settings(
self.db.set_setting("microphone", str(microphone))
if save_audio_to_history is not None:
self.db.set_setting("save_audio_to_history", "true" if save_audio_to_history else "false")
if disable_history_storage is not None:
self.db.set_setting("disable_history_storage", "true" if disable_history_storage else "false")
# Hotkey settings - normalize before storing for consistent format
if hold_hotkey is not None:
self.db.set_setting("hold_hotkey", normalize_hotkey(hold_hotkey))
Expand Down
45 changes: 40 additions & 5 deletions src/components/SettingsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
Hand,
ToggleRight,
HardDrive,
Shield,
} from "lucide-react";
import { api } from "@/lib/api";
import type { Settings, Options, GpuInfo } from "@/lib/types";
Expand Down Expand Up @@ -422,7 +423,41 @@ export function SettingsTab() {
</p>
</BentoSettingCard>

{/* 7. System (Auto Start) (Span 4) */}
{/* 7. Privacy (Span 6) */}
<BentoSettingCard
title="Privacy"
description="Your data, your control"
icon={Shield}
className="md:col-span-6 lg:col-span-6"
>
<div className="space-y-4">
<div className="p-3 rounded-xl bg-secondary/20">
<p className="text-sm text-muted-foreground leading-relaxed">
Voice data stays in RAM only. No network transmission. No disk storage unless history is enabled. All processing happens locally on your device.
</p>
</div>
<div className="flex items-center justify-between p-3 rounded-xl bg-secondary/30 hover:bg-secondary/50 transition-colors">
<Label
htmlFor="disable-history-storage"
className="font-medium cursor-pointer"
>
Disable history storage
</Label>
<Switch
id="disable-history-storage"
checked={settings.disableHistoryStorage}
onCheckedChange={(checked) =>
updateSetting("disableHistoryStorage", checked)
}
/>
</div>
<p className="text-xs text-muted-foreground">
When enabled, transcriptions are pasted but never saved to history for maximum privacy.
</p>
</div>
</BentoSettingCard>

{/* 8. System (Auto Start) (Span 4) */}
<BentoSettingCard
title="System"
description="Startup behavior"
Expand All @@ -446,7 +481,7 @@ export function SettingsTab() {
</div>
</BentoSettingCard>

{/* 8. Data Folder (Span 4) */}
{/* 9. Data Folder (Span 4) */}
<BentoSettingCard
title="Storage"
description="Local data location"
Expand All @@ -464,7 +499,7 @@ export function SettingsTab() {
</div>
</BentoSettingCard>

{/* 8. Keyboard Shortcuts (Full Width) */}
{/* 10. Keyboard Shortcuts (Full Width) */}
<BentoSettingCard
title="Keyboard Shortcuts"
description="Customize recording hotkeys"
Expand Down Expand Up @@ -540,7 +575,7 @@ export function SettingsTab() {
</p>
</div>

{/* 9. GPU / Device (Span 6) */}
{/* 11. GPU / Device (Span 6) */}
<BentoSettingCard
title="Compute Device"
description="Choose CPU or GPU for transcription"
Expand Down Expand Up @@ -614,7 +649,7 @@ export function SettingsTab() {
)}
</BentoSettingCard>

{/* 10. Danger Zone (Span 4) */}
{/* 12. Danger Zone (Span 4) */}
<DangerZoneCard />
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface Settings {
onboardingComplete: boolean;
microphone: number;
saveAudioToHistory: boolean;
disableHistoryStorage: boolean;
// Hotkey settings
holdHotkey: string;
holdHotkeyEnabled: boolean;
Expand Down
14 changes: 14 additions & 0 deletions src/pages/Onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ const StepWelcome = () => (
<span className="headline-serif text-foreground">flow</span>.
</p>

<div className="glass-card p-6 border-primary/20">
<div className="flex items-start gap-4">
<div className="w-12 h-12 shrink-0 rounded-xl bg-primary/10 flex items-center justify-center text-primary border border-primary/20">
<Shield className="w-6 h-6" />
</div>
<div className="space-y-2">
<h3 className="font-semibold text-foreground">Your Privacy is Guaranteed</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
Voice data stays in RAM only. No network transmission. No disk storage unless history is enabled. All processing happens locally on your device.
</p>
</div>
</div>
</div>

<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{ONBOARDING_FEATURES.map((feature) => (
<div
Expand Down
34 changes: 28 additions & 6 deletions src/pages/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,38 @@ export function Popup() {
className="w-screen h-screen flex items-center justify-center select-none"
style={{ background: "transparent" }}
>
{/* IDLE: Tiny pill */}
{/* IDLE: Offline badge */}
{state === "idle" && (
<div
style={{
width: "32px",
height: "4px",
borderRadius: "2px",
background: "rgba(255, 255, 255, 0.15)",
display: "flex",
alignItems: "center",
gap: "6px",
padding: "6px 10px",
borderRadius: "12px",
background: "rgba(0, 0, 0, 0.5)",
backdropFilter: "blur(12px)",
}}
/>
>
<div
style={{
width: "6px",
height: "6px",
borderRadius: "50%",
background: "rgba(156, 163, 175, 0.8)",
}}
/>
<span
style={{
fontSize: "11px",
fontWeight: "500",
color: "rgba(156, 163, 175, 0.9)",
letterSpacing: "0.5px",
}}
>
OFFLINE
</span>
</div>
)}

{/* RECORDING: Simple bars */}
Expand Down