@agentprojectcontext/apx 1.42.2 → 1.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/src/core/config/index.js +2 -0
  3. package/src/core/config/redact.js +2 -0
  4. package/src/core/desktop/process.js +126 -0
  5. package/src/core/voice/stt-hardware.js +87 -0
  6. package/src/core/voice/stt-models.js +97 -0
  7. package/src/core/voice/transcription.js +147 -16
  8. package/src/host/daemon/api/desktop.js +54 -8
  9. package/src/host/daemon/api/transcribe.js +40 -1
  10. package/src/host/daemon/whisper-server.js +18 -8
  11. package/src/host/daemon/whisper-server.py +71 -44
  12. package/src/interfaces/cli/commands/desktop.js +13 -68
  13. package/src/interfaces/desktop/main.js +32 -4
  14. package/src/interfaces/desktop/renderer.js +26 -5
  15. package/src/interfaces/web/dist/assets/index-BAKk7d_M.css +1 -0
  16. package/src/interfaces/web/dist/assets/index-Cjj_d3SA.js +656 -0
  17. package/src/interfaces/web/dist/assets/index-Cjj_d3SA.js.map +1 -0
  18. package/src/interfaces/web/dist/index.html +2 -2
  19. package/src/interfaces/web/src/components/Pager.tsx +88 -0
  20. package/src/interfaces/web/src/components/ShortcutInput.tsx +156 -0
  21. package/src/interfaces/web/src/components/voice/VoiceSttCard.tsx +101 -5
  22. package/src/interfaces/web/src/i18n/en.ts +33 -2
  23. package/src/interfaces/web/src/i18n/es.ts +33 -2
  24. package/src/interfaces/web/src/lib/api/desktop.ts +28 -0
  25. package/src/interfaces/web/src/lib/api/voice.ts +26 -2
  26. package/src/interfaces/web/src/screens/base/GlobalTasksTab.tsx +4 -1
  27. package/src/interfaces/web/src/screens/base/SessionsTab.tsx +4 -1
  28. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +55 -3
  29. package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +98 -36
  30. package/src/interfaces/web/src/screens/project/TasksTab.tsx +4 -1
  31. package/src/interfaces/web/dist/assets/index-BReF4_xV.js +0 -646
  32. package/src/interfaces/web/dist/assets/index-BReF4_xV.js.map +0 -1
  33. package/src/interfaces/web/dist/assets/index-wrEbTJbc.css +0 -1
@@ -18,8 +18,8 @@
18
18
  <link rel="apple-touch-icon" href="/favicon/dark/apple-touch-icon.png" media="(prefers-color-scheme: dark)" />
19
19
  <link rel="manifest" href="/favicon/white/site.webmanifest" media="(prefers-color-scheme: light)" />
20
20
  <link rel="manifest" href="/favicon/dark/site.webmanifest" media="(prefers-color-scheme: dark)" />
21
- <script type="module" crossorigin src="/assets/index-BReF4_xV.js"></script>
22
- <link rel="stylesheet" crossorigin href="/assets/index-wrEbTJbc.css">
21
+ <script type="module" crossorigin src="/assets/index-Cjj_d3SA.js"></script>
22
+ <link rel="stylesheet" crossorigin href="/assets/index-BAKk7d_M.css">
23
23
  </head>
24
24
  <body class="bg-background text-foreground antialiased">
25
25
  <div id="root"></div>
@@ -0,0 +1,88 @@
1
+ import { useEffect, useState } from "react";
2
+ import { ChevronLeft, ChevronRight } from "lucide-react";
3
+ import { Button } from "./ui";
4
+ import { UiSelect } from "./UiSelect";
5
+ import { t } from "../i18n";
6
+
7
+ const DEFAULT_PAGE_SIZE = 20;
8
+ export const PAGE_SIZES = [10, 20, 50, 100];
9
+
10
+ // Client-side pagination over an already-fetched array. The list endpoints
11
+ // return the full set (sessions/tasks are bounded), so we page in the browser
12
+ // rather than round-trip the daemon. Pass `resetKey` (e.g. the active filter)
13
+ // to jump back to page 1 whenever the source set changes; the window is also
14
+ // clamped so a shrinking list never strands the user on an empty page.
15
+ export function usePaged<T>(items: T[], resetKey?: unknown, initialPageSize = DEFAULT_PAGE_SIZE) {
16
+ const [page, setPage] = useState(1);
17
+ const [pageSize, setPageSize] = useState(initialPageSize);
18
+
19
+ useEffect(() => { setPage(1); }, [resetKey]);
20
+
21
+ const pageCount = Math.max(1, Math.ceil(items.length / pageSize));
22
+ const safePage = Math.min(page, pageCount);
23
+ useEffect(() => { if (page !== safePage) setPage(safePage); }, [page, safePage]);
24
+
25
+ const start = (safePage - 1) * pageSize;
26
+ const end = Math.min(start + pageSize, items.length);
27
+ return {
28
+ slice: items.slice(start, end),
29
+ page: safePage,
30
+ pageCount,
31
+ total: items.length,
32
+ start,
33
+ end,
34
+ pageSize,
35
+ setPage,
36
+ // Changing the page size keeps things predictable by returning to page 1.
37
+ setPageSize: (n: number) => { setPageSize(n); setPage(1); },
38
+ };
39
+ }
40
+
41
+ export function Pager({
42
+ page,
43
+ pageCount,
44
+ total,
45
+ start,
46
+ end,
47
+ pageSize,
48
+ onPage,
49
+ onPageSize,
50
+ }: {
51
+ page: number;
52
+ pageCount: number;
53
+ total: number;
54
+ start: number;
55
+ end: number;
56
+ pageSize: number;
57
+ onPage: (p: number) => void;
58
+ onPageSize: (n: number) => void;
59
+ }) {
60
+ // Nothing to page when the whole set fits in the smallest page size.
61
+ if (total <= PAGE_SIZES[0]) return null;
62
+ return (
63
+ <div className="mt-3 flex flex-wrap items-center justify-between gap-3 text-xs text-muted-fg">
64
+ <div className="flex items-center gap-3">
65
+ <span className="tabular-nums">{t("common.pager_range", { from: start + 1, to: end, total })}</span>
66
+ <span className="flex items-center gap-1.5">
67
+ <span>{t("common.pager_per_page")}</span>
68
+ <div className="w-[4.5rem]">
69
+ <UiSelect
70
+ value={String(pageSize)}
71
+ onChange={(v) => onPageSize(Number(v))}
72
+ options={PAGE_SIZES.map((n) => ({ value: String(n), label: String(n) }))}
73
+ />
74
+ </div>
75
+ </span>
76
+ </div>
77
+ <div className="flex items-center gap-1">
78
+ <Button size="sm" variant="ghost" disabled={page <= 1} onClick={() => onPage(page - 1)} aria-label={t("common.pager_prev")}>
79
+ <ChevronLeft size={14} />
80
+ </Button>
81
+ <span className="px-1 tabular-nums">{t("common.pager_page", { page, total: pageCount })}</span>
82
+ <Button size="sm" variant="ghost" disabled={page >= pageCount} onClick={() => onPage(page + 1)} aria-label={t("common.pager_next")}>
83
+ <ChevronRight size={14} />
84
+ </Button>
85
+ </div>
86
+ </div>
87
+ );
88
+ }
@@ -0,0 +1,156 @@
1
+ import { useEffect, useRef, useState, type ReactNode } from "react";
2
+ import { cn } from "../lib/cn";
3
+ import { Kbd } from "./ui/kbd";
4
+ import { t } from "../i18n";
5
+
6
+ // Capture-style field for an Electron global-shortcut accelerator. Instead of
7
+ // showing the raw "CommandOrControl+Shift+G" string in a text box (which reads
8
+ // like gibberish), it renders the combo as readable key badges (⌘ Cmd + G) and
9
+ // lets the user re-record it by clicking and pressing the keys. The stored
10
+ // value stays the canonical Electron accelerator string so main.js /
11
+ // globalShortcut keep working unchanged.
12
+
13
+ const isMac = typeof navigator !== "undefined" && /mac/i.test(navigator.platform || "");
14
+
15
+ // Readable label for one accelerator segment. Modifiers get a glyph + word
16
+ // (a bare "⌘"/"Ctrl" alone reads oddly), other keys (G, Space, Up, F5) show
17
+ // verbatim.
18
+ function tokenLabel(seg: string): string {
19
+ const macMap: Record<string, string> = {
20
+ CommandOrControl: "⌘ Cmd", CmdOrCtrl: "⌘ Cmd", Command: "⌘ Cmd", Cmd: "⌘ Cmd", Super: "⌘ Cmd", Meta: "⌘ Cmd",
21
+ Control: "⌃ Ctrl", Ctrl: "⌃ Ctrl", Option: "⌥ Opt", Alt: "⌥ Opt", Shift: "⇧ Shift",
22
+ };
23
+ const winMap: Record<string, string> = {
24
+ CommandOrControl: "Ctrl", CmdOrCtrl: "Ctrl", Control: "Ctrl", Ctrl: "Ctrl",
25
+ Command: "Win", Cmd: "Win", Super: "Win", Meta: "Win", Option: "Alt", Alt: "Alt", Shift: "Shift",
26
+ };
27
+ const map = isMac ? macMap : winMap;
28
+ return map[seg] ?? seg;
29
+ }
30
+
31
+ function splitAccelerator(value: string): string[] {
32
+ return (value || "").split("+").map((s) => s.trim()).filter(Boolean);
33
+ }
34
+
35
+ // Layout-independent main-key name from a KeyboardEvent.code (so Option-dead
36
+ // keys on mac don't corrupt the letter). Returns null for pure modifier keys.
37
+ function codeToKey(e: KeyboardEvent): string | null {
38
+ const code = e.code;
39
+ if (/^(Shift|Control|Alt|Meta|OS)(Left|Right)?$/.test(code)) return null;
40
+ let m;
41
+ if ((m = code.match(/^Key([A-Z])$/))) return m[1];
42
+ if ((m = code.match(/^Digit(\d)$/))) return m[1];
43
+ if ((m = code.match(/^Numpad(\d)$/))) return m[1];
44
+ if ((m = code.match(/^(F\d{1,2})$/))) return m[1];
45
+ if ((m = code.match(/^Arrow(Up|Down|Left|Right)$/))) return m[1];
46
+ const named: Record<string, string> = {
47
+ Space: "Space", Enter: "Enter", Tab: "Tab", Backspace: "Backspace", Delete: "Delete",
48
+ Home: "Home", End: "End", PageUp: "PageUp", PageDown: "PageDown", Insert: "Insert",
49
+ Minus: "-", Equal: "=", BracketLeft: "[", BracketRight: "]", Backslash: "\\",
50
+ Semicolon: ";", Quote: "'", Comma: ",", Period: ".", Slash: "/", Backquote: "`",
51
+ };
52
+ if (named[code]) return named[code];
53
+ // Fallback: a single printable char from e.key.
54
+ if (e.key && e.key.length === 1) return e.key.toUpperCase();
55
+ return null;
56
+ }
57
+
58
+ // Build a canonical Electron accelerator from a keydown. Returns null until a
59
+ // non-modifier key is pressed alongside ≥1 modifier (or a function key on its
60
+ // own), so a lone ⇧ doesn't get committed.
61
+ function eventToAccelerator(e: KeyboardEvent): string | null {
62
+ const key = codeToKey(e);
63
+ if (!key) return null;
64
+ const parts: string[] = [];
65
+ if (isMac ? e.metaKey : e.ctrlKey) parts.push("CommandOrControl");
66
+ if (isMac && e.ctrlKey) parts.push("Control");
67
+ if (e.altKey) parts.push("Alt");
68
+ if (e.shiftKey) parts.push("Shift");
69
+ const hasMod = parts.length > 0;
70
+ const isFn = /^F\d{1,2}$/.test(key);
71
+ if (!hasMod && !isFn) return null; // need a modifier for letter/digit keys
72
+ parts.push(key);
73
+ return parts.join("+");
74
+ }
75
+
76
+ export function ShortcutInput({
77
+ value,
78
+ onChange,
79
+ disabled,
80
+ className,
81
+ trailing,
82
+ }: {
83
+ value: string;
84
+ onChange: (accelerator: string) => void;
85
+ disabled?: boolean;
86
+ className?: string;
87
+ /** Rendered at the end of the field — e.g. a Save button (input-group style). */
88
+ trailing?: ReactNode;
89
+ }) {
90
+ const [recording, setRecording] = useState(false);
91
+ const ref = useRef<HTMLButtonElement>(null);
92
+
93
+ useEffect(() => {
94
+ if (!recording) return;
95
+ const onKeyDown = (e: KeyboardEvent) => {
96
+ e.preventDefault();
97
+ e.stopPropagation();
98
+ if (e.key === "Escape") { setRecording(false); return; }
99
+ const accel = eventToAccelerator(e);
100
+ if (accel) { onChange(accel); setRecording(false); }
101
+ };
102
+ window.addEventListener("keydown", onKeyDown, true);
103
+ return () => window.removeEventListener("keydown", onKeyDown, true);
104
+ }, [recording, onChange]);
105
+
106
+ const segments = splitAccelerator(value);
107
+
108
+ return (
109
+ <div
110
+ className={cn(
111
+ "flex h-9 w-full max-w-md items-center rounded-md border border-border bg-transparent transition",
112
+ recording && "ring-2 ring-ring",
113
+ disabled && "opacity-50",
114
+ className,
115
+ )}
116
+ >
117
+ {/* Capture area (click → record). Kept borderless so the wrapper reads as
118
+ one input; the trailing slot sits flush at the end. */}
119
+ <button
120
+ ref={ref}
121
+ type="button"
122
+ disabled={disabled}
123
+ onClick={() => setRecording((r) => !r)}
124
+ onBlur={() => setRecording(false)}
125
+ aria-label={t("modules_ui.desktop_shortcut_record")}
126
+ className="flex h-full min-w-0 flex-1 items-center gap-1.5 rounded-l-md px-3 text-sm outline-none hover:bg-muted/40 focus-visible:bg-muted/40"
127
+ >
128
+ {recording ? (
129
+ <span className="text-muted-fg">{t("modules_ui.desktop_shortcut_recording")}</span>
130
+ ) : segments.length ? (
131
+ <span className="flex min-w-0 flex-wrap items-center gap-1">
132
+ {segments.map((seg, i) => (
133
+ <span key={i} className="flex items-center gap-1">
134
+ {i > 0 && <span className="text-xs text-muted-fg">+</span>}
135
+ <Kbd className="h-6 min-w-6 border border-border bg-muted px-1.5 text-[13px] font-semibold text-fg shadow-sm">
136
+ {tokenLabel(seg)}
137
+ </Kbd>
138
+ </span>
139
+ ))}
140
+ </span>
141
+ ) : (
142
+ <span className="text-muted-fg">{t("modules_ui.desktop_shortcut_record")}</span>
143
+ )}
144
+ <span className="ml-auto whitespace-nowrap pl-2 text-xs text-muted-fg">
145
+ {recording ? t("modules_ui.desktop_shortcut_esc") : t("modules_ui.desktop_shortcut_change")}
146
+ </span>
147
+ </button>
148
+
149
+ {trailing ? (
150
+ <div className="flex h-full items-center border-l border-border px-1">
151
+ {trailing}
152
+ </div>
153
+ ) : null}
154
+ </div>
155
+ );
156
+ }
@@ -1,11 +1,16 @@
1
- import { Field } from "../ui";
1
+ import { Field, Input } from "../ui";
2
2
  import { UiSelect } from "../UiSelect";
3
3
  import { WHISPER_MODELS, type TranscriptionConfig } from "../../lib/api/voice";
4
+ import { isSecretMarker, secretSuffix } from "../../lib/secrets";
4
5
  import { t } from "../../i18n";
5
6
 
6
7
  // STT (speech-to-text) configuration. Persisted under config.transcription.
7
- // The actual capture happens in the deck overlay / Telegram / CLI; here the
8
- // owner just picks the backend + (for local whisper) the model + language.
8
+ // The actual capture happens in the desktop window / Telegram / CLI; here the
9
+ // owner picks the engine and configures it:
10
+ // local — embedded faster-whisper (offline; model + language)
11
+ // openai — OpenAI cloud Whisper (api_key + model)
12
+ // custom — any OpenAI-compatible server: mlx-audio on this Mac's Metal GPU,
13
+ // a Radeon/NVIDIA box on the LAN, or a remote endpoint (base_url).
9
14
 
10
15
  interface Props {
11
16
  config: TranscriptionConfig;
@@ -17,6 +22,7 @@ const providerOptions = () => [
17
22
  { value: "auto", label: t("voice_ui.stt_provider_auto") },
18
23
  { value: "local", label: t("voice_ui.stt_provider_local") },
19
24
  { value: "openai", label: t("voice_ui.stt_provider_openai") },
25
+ { value: "custom", label: t("voice_ui.stt_provider_custom") },
20
26
  ];
21
27
 
22
28
  const langOptions = () => [
@@ -32,9 +38,30 @@ const langOptions = () => [
32
38
  export function VoiceSttCard({ config, onPatch, busy }: Props) {
33
39
  const provider = config.provider || "auto";
34
40
  const local = config.local || {};
41
+ const openai = config.openai || {};
42
+ const custom = config.custom || {};
35
43
  const model = local.model || "small";
36
44
  const language = local.language || "auto";
37
- const usesLocal = provider !== "openai";
45
+ // "auto" tries local first, so its tuning shares the local block.
46
+ const showLocal = provider === "auto" || provider === "local";
47
+
48
+ // Text fields patch on blur (not every keystroke), and only when changed.
49
+ const patchText = (key: string, prev: string | undefined, value: string) => {
50
+ const next = value.trim();
51
+ if (next === (prev || "").trim()) return;
52
+ onPatch({ [key]: next });
53
+ };
54
+ // Secrets: a blank field keeps the stored key; the daemon ignores redacted
55
+ // "*** set ***" markers, so we never echo one back as a real value.
56
+ const patchKey = (key: string, value: string) => {
57
+ const next = value.trim();
58
+ if (!next || isSecretMarker(next)) return;
59
+ onPatch({ [key]: next });
60
+ };
61
+ const keyPlaceholder = (marker: unknown) =>
62
+ isSecretMarker(marker)
63
+ ? t("voice_ui.api_key_set", { suffix: secretSuffix(marker) ?? "" })
64
+ : t("voice_ui.api_key_label");
38
65
 
39
66
  return (
40
67
  <div className="space-y-3">
@@ -48,7 +75,7 @@ export function VoiceSttCard({ config, onPatch, busy }: Props) {
48
75
  />
49
76
  </Field>
50
77
 
51
- {usesLocal && (
78
+ {showLocal && (
52
79
  <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
53
80
  <Field label={t("voice_ui.stt_model_label")} hint={t("voice_ui.stt_model_hint")}>
54
81
  <UiSelect
@@ -68,6 +95,75 @@ export function VoiceSttCard({ config, onPatch, busy }: Props) {
68
95
  </Field>
69
96
  </div>
70
97
  )}
98
+
99
+ {provider === "openai" && (
100
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
101
+ <Field
102
+ label={t("voice_ui.api_key_label")}
103
+ hint={isSecretMarker(openai.api_key)
104
+ ? t("voice_ui.api_key_keep_hint")
105
+ : t("voice_ui.api_key_reuse_hint", { engine: "engines.openai.api_key", env: "OPENAI_API_KEY" })}
106
+ >
107
+ <Input
108
+ type="password"
109
+ autoComplete="new-password"
110
+ defaultValue=""
111
+ placeholder={keyPlaceholder(openai.api_key)}
112
+ onBlur={(e) => patchKey("transcription.openai.api_key", e.target.value)}
113
+ disabled={busy}
114
+ />
115
+ </Field>
116
+ <Field label={t("voice_ui.stt_openai_model_label")} hint={t("voice_ui.stt_openai_model_hint")}>
117
+ <Input
118
+ defaultValue={openai.model || ""}
119
+ placeholder="whisper-1"
120
+ onBlur={(e) => patchText("transcription.openai.model", openai.model, e.target.value)}
121
+ disabled={busy}
122
+ />
123
+ </Field>
124
+ </div>
125
+ )}
126
+
127
+ {provider === "custom" && (
128
+ <div className="space-y-3">
129
+ <Field label={t("voice_ui.stt_custom_baseurl_label")} hint={t("voice_ui.stt_custom_baseurl_hint")}>
130
+ <Input
131
+ defaultValue={custom.base_url || ""}
132
+ placeholder="http://localhost:8000/v1"
133
+ onBlur={(e) => patchText("transcription.custom.base_url", custom.base_url, e.target.value)}
134
+ disabled={busy}
135
+ />
136
+ </Field>
137
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
138
+ <Field label={t("voice_ui.stt_custom_model_label")} hint={t("voice_ui.stt_custom_model_hint")}>
139
+ <Input
140
+ defaultValue={custom.model || ""}
141
+ placeholder="mlx-community/whisper-large-v3-turbo"
142
+ onBlur={(e) => patchText("transcription.custom.model", custom.model, e.target.value)}
143
+ disabled={busy}
144
+ />
145
+ </Field>
146
+ <Field label={t("voice_ui.stt_language_label")} hint={t("voice_ui.stt_language_hint")}>
147
+ <UiSelect
148
+ value={custom.language || "auto"}
149
+ onChange={(v) => onPatch({ "transcription.custom.language": v })}
150
+ options={langOptions()}
151
+ disabled={busy}
152
+ />
153
+ </Field>
154
+ </div>
155
+ <Field label={t("voice_ui.api_key_label")} hint={t("voice_ui.stt_custom_key_hint")}>
156
+ <Input
157
+ type="password"
158
+ autoComplete="new-password"
159
+ defaultValue=""
160
+ placeholder={keyPlaceholder(custom.api_key)}
161
+ onBlur={(e) => patchKey("transcription.custom.api_key", e.target.value)}
162
+ disabled={busy}
163
+ />
164
+ </Field>
165
+ </div>
166
+ )}
71
167
  </div>
72
168
  );
73
169
  }
@@ -32,6 +32,11 @@ export const en = {
32
32
  hide: "Hide",
33
33
  copy: "Copy",
34
34
  run: "Run",
35
+ pager_prev: "Previous",
36
+ pager_next: "Next",
37
+ pager_page: "Page {page} of {total}",
38
+ pager_range: "{from}–{to} of {total}",
39
+ pager_per_page:"Per page",
35
40
  },
36
41
  daemon: {
37
42
  connecting: "Connecting to the daemon…",
@@ -940,6 +945,9 @@ export const en = {
940
945
  reload_manifest: "Reload manifest",
941
946
  widget_native: "Native APX widget",
942
947
  widget_external: "External widget",
948
+ preview_badge: "Preview",
949
+ preview_title: "Deck — Coming soon",
950
+ preview_body: "The Deck module is still in development and not released yet. We'll re-enable it once Deck ships in a stable release. For now everything here is read-only and no changes will be saved.",
943
951
  },
944
952
 
945
953
  memory_panel: {
@@ -1150,9 +1158,17 @@ export const en = {
1150
1158
  stt_model_hint: "Bigger = more accurate and slower.",
1151
1159
  stt_language_label: "Language",
1152
1160
  stt_language_hint: "For Spanish, setting \"Spanish\" improves accuracy.",
1153
- stt_provider_auto: "Automatic (local, then OpenAI)",
1161
+ stt_provider_auto: "Automatic (local, then remote)",
1154
1162
  stt_provider_local: "Local — faster-whisper (offline)",
1155
1163
  stt_provider_openai: "OpenAI — Whisper-1 (cloud)",
1164
+ stt_provider_custom: "Custom — OpenAI-compatible server",
1165
+ stt_openai_model_label: "OpenAI model",
1166
+ stt_openai_model_hint: "Defaults to whisper-1.",
1167
+ stt_custom_baseurl_label: "Base URL (OpenAI-compatible)",
1168
+ stt_custom_baseurl_hint: "e.g. http://localhost:8000/v1 (mlx-audio on Metal) or http://192.168.1.50:9000/v1 (Radeon/NVIDIA on the LAN).",
1169
+ stt_custom_model_label: "Model",
1170
+ stt_custom_model_hint: "e.g. mlx-community/whisper-large-v3-turbo or large-v3.",
1171
+ stt_custom_key_hint: "Optional — most local servers need no key.",
1156
1172
  lang_auto: "Auto-detect",
1157
1173
  lang_es: "Spanish",
1158
1174
  lang_en: "English",
@@ -1302,18 +1318,33 @@ export const en = {
1302
1318
  desktop_pos_left: "Left",
1303
1319
  desktop_pos_center: "Center",
1304
1320
  desktop_pos_right: "Right",
1321
+ desktop_theme_system: "System",
1305
1322
  desktop_theme_light: "Light",
1306
1323
  desktop_theme_dark: "Dark",
1307
1324
  desktop_status_desc: "The window launches from the terminal or via autostart.",
1308
1325
  desktop_running: "Running",
1309
1326
  desktop_stopped: "Stopped",
1310
1327
  desktop_refresh: "refresh",
1328
+ desktop_start: "Start",
1329
+ desktop_stop: "Stop",
1330
+ desktop_restart: "Restart",
1331
+ desktop_restart_hint: "Reload the open window so config changes (theme, position) apply now.",
1332
+ desktop_restart_done: "Restarting the window — applying the latest config.",
1333
+ desktop_restart_none: "No desktop window is connected.",
1334
+ desktop_start_done: "Desktop window launched.",
1335
+ desktop_start_already: "Desktop window is already running.",
1336
+ desktop_stop_done: "Desktop window stopped.",
1337
+ desktop_stop_none: "No desktop window was running.",
1311
1338
  desktop_from_terminal: "From terminal:",
1312
1339
  desktop_autostart_desc: "Launches the window at user login. Equivalent to `apx desktop install` (no sudo required).",
1313
1340
  desktop_platform: "platform: {platform}",
1314
1341
  desktop_shortcut_desc: "Global hotkey that shows/hides the window and starts listening.",
1315
1342
  desktop_accelerator: "Accelerator",
1316
- desktop_accelerator_hint:"Electron format, e.g. \"CommandOrControl+G\" or \"CommandOrControl+Shift+Space\". Restart the window to apply.",
1343
+ desktop_accelerator_hint:"Click the field and press your key combo. Restart the window to apply.",
1344
+ desktop_shortcut_record: "Click to set a shortcut",
1345
+ desktop_shortcut_recording: "Press your combo…",
1346
+ desktop_shortcut_change: "click to change",
1347
+ desktop_shortcut_esc: "Esc to cancel",
1317
1348
  desktop_shortcut_saved: "Shortcut saved. Restart the window (apx desktop stop && start) to apply it.",
1318
1349
  desktop_autostart_on: "Autostart enabled for the next login.",
1319
1350
  desktop_autostart_off: "Autostart disabled.",
@@ -33,6 +33,11 @@ export const es = {
33
33
  hide: "Ocultar",
34
34
  copy: "Copiar",
35
35
  run: "Ejecutar",
36
+ pager_prev: "Anterior",
37
+ pager_next: "Siguiente",
38
+ pager_page: "Página {page} de {total}",
39
+ pager_range: "{from}–{to} de {total}",
40
+ pager_per_page:"Por página",
36
41
  },
37
42
  daemon: {
38
43
  connecting: "Conectando con el daemon…",
@@ -938,6 +943,9 @@ export const es = {
938
943
  reload_manifest: "Recargar manifest",
939
944
  widget_native: "Widget nativo APX",
940
945
  widget_external: "Widget externo",
946
+ preview_badge: "Vista previa",
947
+ preview_title: "Deck — Próximamente",
948
+ preview_body: "El módulo Deck todavía está en desarrollo y no fue lanzado aún. Lo volveremos a activar cuando Deck salga en una versión estable. Por ahora todo acá es de solo lectura y no se guardará ningún cambio.",
941
949
  },
942
950
 
943
951
  memory_panel: {
@@ -1148,9 +1156,17 @@ export const es = {
1148
1156
  stt_model_hint: "Más grande = más preciso y más lento.",
1149
1157
  stt_language_label: "Idioma",
1150
1158
  stt_language_hint: "Para español, elegir \"Español\" mejora la precisión.",
1151
- stt_provider_auto: "Automático (local, después OpenAI)",
1159
+ stt_provider_auto: "Automático (local, después remoto)",
1152
1160
  stt_provider_local: "Local — faster-whisper (offline)",
1153
1161
  stt_provider_openai: "OpenAI — Whisper-1 (cloud)",
1162
+ stt_provider_custom: "Custom — server OpenAI-compatible",
1163
+ stt_openai_model_label: "Modelo OpenAI",
1164
+ stt_openai_model_hint: "Por defecto whisper-1.",
1165
+ stt_custom_baseurl_label: "URL base (OpenAI-compatible)",
1166
+ stt_custom_baseurl_hint: "Ej: http://localhost:8000/v1 (mlx-audio en Metal) o http://192.168.1.50:9000/v1 (Radeon/NVIDIA en la red).",
1167
+ stt_custom_model_label: "Modelo",
1168
+ stt_custom_model_hint: "Ej: mlx-community/whisper-large-v3-turbo o large-v3.",
1169
+ stt_custom_key_hint: "Opcional — la mayoría de los servers locales no requieren key.",
1154
1170
  lang_auto: "Detección automática",
1155
1171
  lang_es: "Español",
1156
1172
  lang_en: "Inglés",
@@ -1300,18 +1316,33 @@ export const es = {
1300
1316
  desktop_pos_left: "Izquierda",
1301
1317
  desktop_pos_center: "Centro",
1302
1318
  desktop_pos_right: "Derecha",
1319
+ desktop_theme_system: "Sistema",
1303
1320
  desktop_theme_light: "Claro",
1304
1321
  desktop_theme_dark: "Oscuro",
1305
1322
  desktop_status_desc: "La ventana se abre desde la terminal o por arranque automático.",
1306
1323
  desktop_running: "Corriendo",
1307
1324
  desktop_stopped: "Detenida",
1308
1325
  desktop_refresh: "refrescar",
1326
+ desktop_start: "Iniciar",
1327
+ desktop_stop: "Detener",
1328
+ desktop_restart: "Reiniciar",
1329
+ desktop_restart_hint: "Recarga la ventana abierta para aplicar ya los cambios de config (tema, posición).",
1330
+ desktop_restart_done: "Reiniciando la ventana — aplicando la última config.",
1331
+ desktop_restart_none: "No hay ninguna ventana de desktop conectada.",
1332
+ desktop_start_done: "Ventana de desktop iniciada.",
1333
+ desktop_start_already: "La ventana de desktop ya estaba corriendo.",
1334
+ desktop_stop_done: "Ventana de desktop detenida.",
1335
+ desktop_stop_none: "No había ninguna ventana de desktop corriendo.",
1309
1336
  desktop_from_terminal: "Desde la terminal:",
1310
1337
  desktop_autostart_desc: "Abre la ventana al iniciar sesión. Equivale a `apx desktop install` (no requiere sudo).",
1311
1338
  desktop_platform: "plataforma: {platform}",
1312
1339
  desktop_shortcut_desc: "Atajo global que muestra/oculta la ventana y empieza a escuchar.",
1313
1340
  desktop_accelerator: "Acelerador",
1314
- desktop_accelerator_hint:"Formato Electron, ej. \"CommandOrControl+G\" o \"CommandOrControl+Shift+Space\". Reiniciá la ventana para aplicar.",
1341
+ desktop_accelerator_hint:"Hacé clic en el campo y apretá tu combinación de teclas. Reiniciá la ventana para aplicar.",
1342
+ desktop_shortcut_record: "Hacé clic para definir un atajo",
1343
+ desktop_shortcut_recording: "Apretá tu combinación…",
1344
+ desktop_shortcut_change: "clic para cambiar",
1345
+ desktop_shortcut_esc: "Esc para cancelar",
1315
1346
  desktop_shortcut_saved: "Atajo guardado. Reiniciá la ventana (apx desktop stop && start) para aplicarlo.",
1316
1347
  desktop_autostart_on: "Arranque automático habilitado para el próximo inicio de sesión.",
1317
1348
  desktop_autostart_off: "Arranque automático deshabilitado.",
@@ -16,6 +16,25 @@ export interface DesktopStatus {
16
16
  running: boolean;
17
17
  }
18
18
 
19
+ export interface DesktopRestartResult {
20
+ ok: boolean;
21
+ /** How many connected windows were told to reload (0 = none connected). */
22
+ reloaded: number;
23
+ }
24
+
25
+ export interface DesktopStartResult {
26
+ ok: boolean;
27
+ pid?: number;
28
+ /** True when a window was already running (start was a no-op). */
29
+ already?: boolean;
30
+ }
31
+
32
+ export interface DesktopStopResult {
33
+ ok: boolean;
34
+ /** False when nothing was running. */
35
+ stopped?: boolean;
36
+ }
37
+
19
38
  export interface AutostartStatus {
20
39
  ok: boolean;
21
40
  enabled: boolean;
@@ -26,6 +45,15 @@ export const Desktop = {
26
45
  /** GET /desktop/status — connected window count + running flag (live probe). */
27
46
  status: () => http.get<DesktopStatus>("/desktop/status"),
28
47
 
48
+ /** POST /desktop/start — launch the floating window (detached Electron). */
49
+ start: () => http.post<DesktopStartResult>("/desktop/start", {}),
50
+
51
+ /** POST /desktop/stop — terminate the running window. */
52
+ stop: () => http.post<DesktopStopResult>("/desktop/stop", {}),
53
+
54
+ /** POST /desktop/restart — tell every live window to reload + re-read config. */
55
+ restart: () => http.post<DesktopRestartResult>("/desktop/restart", {}),
56
+
29
57
  /** GET /desktop/autostart — current login-item state for this platform. */
30
58
  autostartGet: () => http.get<AutostartStatus>("/desktop/autostart"),
31
59
 
@@ -99,9 +99,33 @@ export interface TranscriptionLocalConfig {
99
99
  beam_size?: number;
100
100
  idle_minutes?: number;
101
101
  }
102
+ export interface TranscriptionOpenAIConfig {
103
+ base_url?: string; // defaults to https://api.openai.com/v1
104
+ api_key?: string; // may carry a redacted "*** set ***" marker
105
+ model?: string; // defaults to whisper-1
106
+ }
107
+ export interface TranscriptionCustomConfig {
108
+ base_url?: string; // OpenAI-compatible server, e.g. http://localhost:8000/v1
109
+ api_key?: string; // optional; may carry a redacted marker
110
+ model?: string; // e.g. mlx-community/whisper-large-v3-turbo
111
+ language?: string; // ISO code or "auto"
112
+ }
102
113
  export interface TranscriptionConfig {
103
- provider?: string; // "auto" | "local" | "openai"
114
+ provider?: string; // "auto" | "local" | "openai" | "custom"
104
115
  local?: TranscriptionLocalConfig;
116
+ openai?: TranscriptionOpenAIConfig;
117
+ custom?: TranscriptionCustomConfig;
118
+ }
119
+
120
+ /** One STT engine entry as reported by GET /transcribe/providers. */
121
+ export interface SttProviderEntry {
122
+ id: string; // "local" | "openai" | "custom"
123
+ available: boolean;
124
+ configured: boolean;
125
+ }
126
+ export interface SttProvidersResponse {
127
+ configured_provider: string;
128
+ engines: SttProviderEntry[];
105
129
  }
106
130
 
107
131
  // Known engine voice presets used to fill selects without a daemon round-trip.
@@ -109,7 +133,7 @@ export const OPENAI_TTS_VOICES = ["alloy", "echo", "fable", "onyx", "nova", "shi
109
133
  export const GEMINI_TTS_VOICES = ["Kore", "Puck", "Charon", "Fenrir", "Aoede"];
110
134
  export const ELEVENLABS_MODELS = ["eleven_multilingual_v2", "eleven_turbo_v2_5", "eleven_flash_v2_5"];
111
135
  export const OPENAI_TTS_MODELS = ["tts-1", "tts-1-hd"];
112
- export const WHISPER_MODELS = ["tiny", "base", "small", "medium", "large-v2", "large-v3"];
136
+ export const WHISPER_MODELS = ["tiny", "base", "small", "medium", "large-v2", "large-v3", "large-v3-turbo"];
113
137
 
114
138
  // Friendly labels + ordering for the provider list. The daemon is the source
115
139
  // of truth for availability; this only adds display names + a stable order.