@agentprojectcontext/apx 1.43.0 → 1.45.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.
@@ -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-B0nTYflm.js"></script>
22
- <link rel="stylesheet" crossorigin href="/assets/index-C22PmKCD.css">
21
+ <script type="module" crossorigin src="/assets/index-D7px5xcy.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>
@@ -5,9 +5,7 @@ import { ProjectSidebar, projectKindLabel } from "./components/layout/ProjectSid
5
5
  import { ApxAdminScreen } from "./screens/ApxAdminScreen";
6
6
  import { ProjectScreen } from "./screens/ProjectScreen";
7
7
  import { SettingsScreen } from "./screens/SettingsScreen";
8
- import { VoiceScreen } from "./screens/modules/VoiceScreen";
9
8
  import { DesktopScreen } from "./screens/modules/DesktopScreen";
10
- import { DeckScreen } from "./screens/modules/DeckScreen";
11
9
  import { CodeScreen } from "./screens/modules/CodeScreen";
12
10
  import { AddProjectDialog } from "./components/AddProjectDialog";
13
11
  import { PairingScreen } from "./screens/PairingScreen";
@@ -92,9 +90,7 @@ function Shell() {
92
90
  <Routes>
93
91
  <Route path="/" element={<ApxAdminScreen />} />
94
92
  <Route path="/settings/*" element={<SettingsScreen />} />
95
- <Route path="/m/voice/*" element={<VoiceScreen />} />
96
93
  <Route path="/m/desktop/*" element={<DesktopScreen />} />
97
- <Route path="/m/deck/*" element={<DeckScreen />} />
98
94
  <Route path="/m/code/*" element={<CodeScreen />} />
99
95
  <Route path="/p/:pid/*" element={<ProjectScreen />} />
100
96
  <Route path="*" element={<NotFound />} />
@@ -208,9 +204,7 @@ function LanguageMenu() {
208
204
 
209
205
  function moduleLabel(key?: string) {
210
206
  switch (key) {
211
- case "voice": return t("nav.modules.voice");
212
207
  case "desktop": return t("nav.modules.desktop");
213
- case "deck": return t("nav.modules.deck");
214
208
  case "code": return t("nav.modules.code");
215
209
  default: return key || "";
216
210
  }
@@ -222,6 +216,9 @@ function settingsLabel(key?: string) {
222
216
  case "engines": return t("settings.tabs.engines");
223
217
  case "telegram": return t("settings.tabs.telegram");
224
218
  case "devices": return t("settings.tabs.devices");
219
+ case "voice": return t("nav.modules.voice");
220
+ case "deck": return t("nav.modules.deck");
221
+ case "desktop": return t("nav.modules.desktop");
225
222
  case "appearance": return t("settings.appearance");
226
223
  case "config":
227
224
  case "advanced": return t("settings.tabs.advanced");
@@ -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,116 @@
1
+ import { useState } from "react";
2
+ import { Link } from "react-router-dom";
3
+ import useSWR from "swr";
4
+ import { Settings } from "lucide-react";
5
+ import { Section, Kbd, StatusDot } from "../Section";
6
+ import { Button, Loading } from "../ui";
7
+ import { useToast } from "../Toast";
8
+ import { Desktop } from "../../lib/api/desktop";
9
+ import { t } from "../../i18n";
10
+
11
+ // Live status of the floating Desktop window + lifecycle controls
12
+ // (start/stop/restart). Shared between the Desktop rail module and the
13
+ // Settings → Desktop panel so both surfaces keep the same action card.
14
+ // Pass `showConfigLink` on the rail module to link into the settings panel;
15
+ // inside Settings it's redundant, so it's omitted there.
16
+ export function DesktopStatusCard({ showConfigLink = false }: { showConfigLink?: boolean }) {
17
+ const toast = useToast();
18
+
19
+ const { data: status, isLoading: stLoading, mutate: mutateStatus } = useSWR(
20
+ "/desktop/status",
21
+ () => Desktop.status(),
22
+ { refreshInterval: 5000 },
23
+ );
24
+ const running = !!status?.running;
25
+
26
+ // Which lifecycle action (start/stop/restart) is in flight — drives the
27
+ // per-button spinner and disables its siblings while one runs.
28
+ const [lifeAction, setLifeAction] = useState<"start" | "stop" | "restart" | null>(null);
29
+
30
+ // Start/Stop launch or kill the Electron window (daemon spawns/SIGTERMs it);
31
+ // Restart tells a live window to reload + re-read config (theme, position,
32
+ // shortcut) — the "apply now" the static status-poll never did. All three
33
+ // re-poll status shortly after so the dot + buttons settle.
34
+ const runLifecycle = async (action: "start" | "stop" | "restart", fn: () => Promise<void>) => {
35
+ setLifeAction(action);
36
+ try { await fn(); }
37
+ catch (e) { toast.error((e as Error).message); }
38
+ finally { setLifeAction(null); setTimeout(() => mutateStatus(), 1200); }
39
+ };
40
+ const startDesktop = () => runLifecycle("start", async () => {
41
+ const r = await Desktop.start();
42
+ toast.success(r.already ? t("modules_ui.desktop_start_already") : t("modules_ui.desktop_start_done"));
43
+ });
44
+ const stopDesktop = () => runLifecycle("stop", async () => {
45
+ const r = await Desktop.stop();
46
+ toast.success(r.stopped ? t("modules_ui.desktop_stop_done") : t("modules_ui.desktop_stop_none"));
47
+ });
48
+ const restartDesktop = () => runLifecycle("restart", async () => {
49
+ const r = await Desktop.restart();
50
+ if (r.reloaded > 0) toast.success(t("modules_ui.desktop_restart_done"));
51
+ else toast.info(t("modules_ui.desktop_restart_none"));
52
+ });
53
+
54
+ return (
55
+ <Section
56
+ title={t("desktop_screen.status_title")}
57
+ description={t("modules_ui.desktop_status_desc")}
58
+ action={
59
+ <div className="flex items-center gap-2">
60
+ <Button
61
+ variant="primary"
62
+ size="sm"
63
+ onClick={startDesktop}
64
+ loading={lifeAction === "start"}
65
+ disabled={running || (lifeAction !== null && lifeAction !== "start")}
66
+ >
67
+ {t("modules_ui.desktop_start")}
68
+ </Button>
69
+ <Button
70
+ variant="secondary"
71
+ size="sm"
72
+ onClick={stopDesktop}
73
+ loading={lifeAction === "stop"}
74
+ disabled={!running || (lifeAction !== null && lifeAction !== "stop")}
75
+ >
76
+ {t("modules_ui.desktop_stop")}
77
+ </Button>
78
+ <Button
79
+ variant="secondary"
80
+ size="sm"
81
+ onClick={restartDesktop}
82
+ loading={lifeAction === "restart"}
83
+ disabled={!running || (lifeAction !== null && lifeAction !== "restart")}
84
+ title={t("modules_ui.desktop_restart_hint")}
85
+ >
86
+ {t("modules_ui.desktop_restart")}
87
+ </Button>
88
+ {showConfigLink && (
89
+ <Link to="/settings/desktop">
90
+ <Button size="sm" variant="ghost">
91
+ <Settings size={14} /> {t("desktop_screen.open_config")}
92
+ </Button>
93
+ </Link>
94
+ )}
95
+ </div>
96
+ }
97
+ >
98
+ {stLoading ? <Loading /> : (
99
+ <div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm">
100
+ <StatusDot ok={running} />
101
+ <span className="font-medium">{running ? t("modules_ui.desktop_running") : t("modules_ui.desktop_stopped")}</span>
102
+ <button
103
+ type="button"
104
+ onClick={() => mutateStatus()}
105
+ className="text-xs text-muted-fg underline-offset-2 hover:underline"
106
+ >
107
+ {t("modules_ui.desktop_refresh")}
108
+ </button>
109
+ <span className="text-xs text-muted-fg">
110
+ ({t("modules_ui.desktop_from_terminal")} <Kbd>apx desktop start</Kbd> · <Kbd>apx desktop --debug</Kbd>)
111
+ </span>
112
+ </div>
113
+ )}
114
+ </Section>
115
+ );
116
+ }
@@ -1,7 +1,7 @@
1
- // Discord-style left rail. Logo on top (APX admin), then Base, then the
2
- // rail-level MODULES (Voice/Deck/Code) that sit alongside Base, then the
3
- // projects column, finally add + settings. The default workspace (id=0) is
4
- // pinned first.
1
+ // Discord-style left rail. Logo on top (APX admin), then Base together with the
2
+ // rail-level MODULES (Desktop/Code) as one group, then the projects column,
3
+ // finally add + settings. The default workspace (id=0) is pinned first.
4
+ // Voice and Deck used to live here too — they now live inside Settings.
5
5
  //
6
6
  // The projects column is the only flexible zone: top (logo/base/modules) and
7
7
  // bottom (add/settings/docs/roby) stay pinned. Projects are listed newest-first
@@ -10,7 +10,7 @@
10
10
  // also be collapsed into a single folder button (state persisted per browser).
11
11
  import { useLayoutEffect, useRef, useState } from "react";
12
12
  import { useLocation } from "react-router-dom";
13
- import { Plus, Settings, Mic, Monitor, LayoutGrid, Terminal, Bot, BookOpen, ChevronDown, Folders, type LucideIcon } from "lucide-react";
13
+ import { Plus, Settings, Monitor, Terminal, Bot, BookOpen, ChevronDown, Folders, type LucideIcon } from "lucide-react";
14
14
  import { Logo } from "./Logo";
15
15
  import { ProjectAvatar, projectTone } from "./ProjectAvatar";
16
16
  import { Tip } from "../ui/tip";
@@ -45,10 +45,8 @@ interface ModuleItem {
45
45
  // top-level entry next to Base rather than living inside Settings.
46
46
  function buildModules(): ModuleItem[] {
47
47
  return [
48
- { id: "voice", label: t("nav.modules.voice"), href: "/m/voice", icon: Mic },
49
48
  { id: "desktop", label: t("nav.modules.desktop"), href: "/m/desktop", icon: Monitor },
50
- { id: "deck", label: t("nav.modules.deck"), href: "/m/deck", icon: LayoutGrid },
51
- { id: "code", label: t("nav.modules.code"), href: "/m/code", icon: Terminal },
49
+ { id: "code", label: t("nav.modules.code"), href: "/m/code", icon: Terminal },
52
50
  ];
53
51
  }
54
52
 
@@ -207,8 +205,7 @@ export function ProjectSidebar({ onSelect, onOpenRoby, onOpenAddProject }: Props
207
205
  />
208
206
  )}
209
207
 
210
- {/* Modules — rail-level surfaces alongside Base. */}
211
- <div className="my-0.5 h-px w-8 rounded-full bg-border" />
208
+ {/* Modules — rail-level surfaces grouped with Base (no divider). */}
212
209
  {MODULES.map((m) => (
213
210
  <ProjectAvatar
214
211
  key={m.id}
@@ -0,0 +1,185 @@
1
+ import { useEffect, useState } from "react";
2
+ import { Link } from "react-router-dom";
3
+ import useSWR from "swr";
4
+ import { Section } from "../Section";
5
+ import { Button, Field, Switch, Loading } from "../ui";
6
+ import { UiSelect } from "../UiSelect";
7
+ import { ShortcutInput } from "../ShortcutInput";
8
+ import { DesktopStatusCard } from "../desktop/DesktopStatusCard";
9
+ import { useToast } from "../Toast";
10
+ import { useGlobalConfig } from "../../hooks/useGlobalConfig";
11
+ import { Desktop } from "../../lib/api/desktop";
12
+ import { t } from "../../i18n";
13
+
14
+ const DEFAULT_SHORTCUT = "CommandOrControl+G";
15
+ const positionOpts = () => [
16
+ { value: "left", label: t("modules_ui.desktop_pos_left") },
17
+ { value: "center", label: t("modules_ui.desktop_pos_center") },
18
+ { value: "right", label: t("modules_ui.desktop_pos_right") },
19
+ ];
20
+ const themeOpts = () => [
21
+ { value: "system", label: t("modules_ui.desktop_theme_system") },
22
+ { value: "light", label: t("modules_ui.desktop_theme_light") },
23
+ { value: "dark", label: t("modules_ui.desktop_theme_dark") },
24
+ ];
25
+
26
+ // Desktop configuration — the persisted settings for the floating voice window
27
+ // (autostart, global shortcut, appearance, activation). Lives in Settings; the
28
+ // Desktop rail module only shows live status + the last conversation. The window
29
+ // itself is an Electron process spawned by `apx desktop start`, so this just
30
+ // edits config the daemon persists and toggles per-user autostart.
31
+ export function DesktopSettingsPanel() {
32
+ const toast = useToast();
33
+ const { config, isLoading: cfgLoading, patch } = useGlobalConfig();
34
+
35
+ // config.desktop isn't on the typed GlobalConfig — read it off a local view.
36
+ const cfgView = config as unknown as {
37
+ desktop?: {
38
+ shortcut?: string; enabled?: boolean;
39
+ theme?: "light" | "dark" | "system";
40
+ position?: "left" | "center" | "right";
41
+ };
42
+ overlay?: { shortcut?: string }; // legacy fallback
43
+ };
44
+ const savedShortcut = cfgView.desktop?.shortcut || cfgView.overlay?.shortcut || DEFAULT_SHORTCUT;
45
+ const enabled = cfgView.desktop?.enabled !== false;
46
+ // Default to "system" so the window follows the OS appearance until the
47
+ // user explicitly pins light/dark.
48
+ const theme = cfgView.desktop?.theme || "system";
49
+ const position = cfgView.desktop?.position || "right";
50
+
51
+ const { data: autostart, mutate: mutateAutostart } = useSWR(
52
+ "/desktop/autostart",
53
+ () => Desktop.autostartGet(),
54
+ );
55
+
56
+ const [shortcut, setShortcut] = useState(savedShortcut);
57
+ const [busy, setBusy] = useState(false);
58
+ const [autostartBusy, setAutostartBusy] = useState(false);
59
+ useEffect(() => setShortcut(savedShortcut), [savedShortcut]);
60
+
61
+ const saveShortcut = async () => {
62
+ const next = shortcut.trim();
63
+ if (!next || next === savedShortcut) return;
64
+ setBusy(true);
65
+ try {
66
+ await patch({ "desktop.shortcut": next });
67
+ toast.success(t("modules_ui.desktop_shortcut_saved"));
68
+ } catch (e) { toast.error((e as Error).message); }
69
+ finally { setBusy(false); }
70
+ };
71
+
72
+ const patchKey = async (key: string, value: unknown, ok: string) => {
73
+ setBusy(true);
74
+ try {
75
+ await patch({ [key]: value });
76
+ toast.success(ok);
77
+ } catch (e) { toast.error((e as Error).message); }
78
+ finally { setBusy(false); }
79
+ };
80
+
81
+ const toggleAutostart = async (v: boolean) => {
82
+ setAutostartBusy(true);
83
+ try {
84
+ await Desktop.autostartSet(v);
85
+ await mutateAutostart();
86
+ toast.success(v ? t("modules_ui.desktop_autostart_on") : t("modules_ui.desktop_autostart_off"));
87
+ } catch (e) { toast.error((e as Error).message); }
88
+ finally { setAutostartBusy(false); }
89
+ };
90
+
91
+ return (
92
+ <div className="space-y-6" data-testid="settings-desktop">
93
+ <DesktopStatusCard />
94
+
95
+ <Section
96
+ title={t("desktop_screen.autostart_title")}
97
+ description={t("modules_ui.desktop_autostart_desc")}
98
+ >
99
+ {!autostart ? <Loading /> : (
100
+ <div className="flex items-center justify-between gap-3">
101
+ <Switch
102
+ checked={autostart.enabled}
103
+ onChange={toggleAutostart}
104
+ disabled={autostartBusy}
105
+ label={autostart.enabled ? t("common.enabled") : t("common.disabled")}
106
+ />
107
+ <span className="text-xs text-muted-fg">{t("modules_ui.desktop_platform", { platform: autostart.platform })}</span>
108
+ </div>
109
+ )}
110
+ </Section>
111
+
112
+ <Section
113
+ title={t("desktop_screen.shortcut_title")}
114
+ description={t("modules_ui.desktop_shortcut_desc")}
115
+ >
116
+ {cfgLoading ? <Loading /> : (
117
+ <Field
118
+ label={t("modules_ui.desktop_accelerator")}
119
+ hint={t("modules_ui.desktop_accelerator_hint")}
120
+ >
121
+ <ShortcutInput
122
+ value={shortcut}
123
+ onChange={setShortcut}
124
+ disabled={busy}
125
+ trailing={
126
+ <Button
127
+ variant="primary"
128
+ size="sm"
129
+ onClick={saveShortcut}
130
+ loading={busy}
131
+ disabled={!shortcut.trim() || shortcut.trim() === savedShortcut}
132
+ >
133
+ {t("common.save")}
134
+ </Button>
135
+ }
136
+ />
137
+ </Field>
138
+ )}
139
+ </Section>
140
+
141
+ <Section title={t("desktop_screen.appearance_title")} description={t("modules_ui.desktop_appearance_desc")}>
142
+ {cfgLoading ? <Loading /> : (
143
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
144
+ <Field label={t("modules_ui.desktop_theme")} hint={t("modules_ui.desktop_restart_apply")}>
145
+ <UiSelect
146
+ value={theme}
147
+ onChange={(v) => patchKey("desktop.theme", v, t("modules_ui.desktop_theme_set", { value: v }))}
148
+ options={themeOpts()}
149
+ disabled={busy}
150
+ />
151
+ </Field>
152
+ <Field label={t("modules_ui.desktop_position")} hint={t("modules_ui.desktop_position_hint")}>
153
+ <UiSelect
154
+ value={position}
155
+ onChange={(v) => patchKey("desktop.position", v, t("modules_ui.desktop_position_set", { value: v }))}
156
+ options={positionOpts()}
157
+ disabled={busy}
158
+ />
159
+ </Field>
160
+ </div>
161
+ )}
162
+ </Section>
163
+
164
+ <Section
165
+ title={t("desktop_screen.activation_title")}
166
+ description={t("modules_ui.desktop_activation_desc")}
167
+ >
168
+ {cfgLoading ? <Loading /> : (
169
+ <div className="space-y-3">
170
+ <Switch
171
+ checked={enabled}
172
+ onChange={(v) => patchKey("desktop.enabled", v, v ? t("modules_ui.desktop_enabled_toast") : t("modules_ui.desktop_disabled_toast"))}
173
+ disabled={busy}
174
+ label={enabled ? t("modules_ui.desktop_plugin_on") : t("modules_ui.desktop_plugin_off")}
175
+ />
176
+ <p className="text-xs text-muted-fg">
177
+ {t("modules_ui.desktop_stt_engine")} <Link to="/settings/voice" className="font-medium text-fg underline underline-offset-2">{t("nav.modules.voice")}</Link>{" "}
178
+ {t("modules_ui.desktop_stt_engine_suffix")}
179
+ </p>
180
+ </div>
181
+ )}
182
+ </Section>
183
+ </div>
184
+ );
185
+ }
@@ -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…",
@@ -175,6 +180,7 @@ export const en = {
175
180
  account_section: "Account",
176
181
  agents_section: "Agents & models",
177
182
  channels_section: "Channels & devices",
183
+ modules_section: "Modules",
178
184
  advanced_section: "Advanced",
179
185
 
180
186
  tabs: {
@@ -925,6 +931,7 @@ export const en = {
925
931
  appearance_title: "Appearance",
926
932
  activation_title: "Activation + transcription",
927
933
  last_conv_title: "Last conversation",
934
+ open_config: "Configuration",
928
935
  },
929
936
 
930
937
  voice_screen: {
@@ -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…",
@@ -176,6 +181,7 @@ export const es = {
176
181
  account_section: "Cuenta",
177
182
  agents_section: "Agentes & modelos",
178
183
  channels_section: "Canales & dispositivos",
184
+ modules_section: "Módulos",
179
185
  advanced_section: "Avanzado",
180
186
 
181
187
  tabs: {
@@ -923,6 +929,7 @@ export const es = {
923
929
  appearance_title: "Apariencia",
924
930
  activation_title: "Activación + transcripción",
925
931
  last_conv_title: "Última conversación",
932
+ open_config: "Configuración",
926
933
  },
927
934
 
928
935
  voice_screen: {
@@ -1,7 +1,7 @@
1
1
  import { type ReactElement } from "react";
2
2
  import { useLocation, useNavigate } from "react-router-dom";
3
3
  import {
4
- Bot, Cpu, Database, KeyRound, MessageCircle, Palette, ScrollText, Send, Smartphone, Sparkles, User,
4
+ Bot, Cpu, Database, KeyRound, LayoutGrid, MessageCircle, Mic, Monitor, Palette, ScrollText, Send, Smartphone, Sparkles, User,
5
5
  } from "lucide-react";
6
6
  import { useNavCollapse, type TabSection } from "../components/common/TabNav";
7
7
  import { TabLayout } from "../components/common/TabLayout";
@@ -14,11 +14,15 @@ import { TelegramSettingsTabs } from "../components/settings/TelegramSettingsTab
14
14
  import { DevicesPanel } from "../components/settings/DevicesPanel";
15
15
  import { AdvancedPanel } from "../components/settings/AdvancedPanel";
16
16
  import { AppearancePanel } from "../components/settings/AppearancePanel";
17
+ import { DesktopSettingsPanel } from "../components/settings/DesktopSettingsPanel";
18
+ import { VoiceScreen } from "./modules/VoiceScreen";
19
+ import { DeckScreen } from "./modules/DeckScreen";
17
20
  import { STORAGE } from "../constants";
18
21
  import { t } from "../i18n";
19
22
 
20
23
  type TabKey =
21
- | "identity" | "super_agent" | "engines" | "memory" | "skills" | "telegram" | "devices" | "appearance" | "advanced";
24
+ | "identity" | "super_agent" | "engines" | "memory" | "skills" | "telegram" | "devices"
25
+ | "voice" | "deck" | "desktop" | "appearance" | "advanced";
22
26
 
23
27
  const SECTIONS: TabSection[] = [
24
28
  {
@@ -44,6 +48,14 @@ const SECTIONS: TabSection[] = [
44
48
  { key: "devices", label: t("settings.tabs.devices"), icon: Smartphone },
45
49
  ],
46
50
  },
51
+ {
52
+ title: t("settings.modules_section"),
53
+ items: [
54
+ { key: "voice", label: t("nav.modules.voice"), icon: Mic },
55
+ { key: "deck", label: t("nav.modules.deck"), icon: LayoutGrid },
56
+ { key: "desktop", label: t("nav.modules.desktop"), icon: Monitor },
57
+ ],
58
+ },
47
59
  {
48
60
  title: t("settings.advanced_section"),
49
61
  items: [
@@ -56,7 +68,7 @@ const SECTIONS: TabSection[] = [
56
68
  // on xl (and so wants full available width). Single-section panels (identity,
57
69
  // super agent, devices, advanced) keep a cosier reading width so wide displays
58
70
  // don't blow form fields up to absurd widths.
59
- const WIDE_TABS = new Set<TabKey>(["engines", "telegram", "memory", "skills", "appearance"]);
71
+ const WIDE_TABS = new Set<TabKey>(["engines", "telegram", "memory", "skills", "appearance", "voice"]);
60
72
 
61
73
  const PANELS: Record<TabKey, () => ReactElement> = {
62
74
  identity: () => <IdentityPanel />,
@@ -66,6 +78,9 @@ const PANELS: Record<TabKey, () => ReactElement> = {
66
78
  skills: () => <SkillsInspectorPanel />,
67
79
  telegram: () => <TelegramSettingsTabs />,
68
80
  devices: () => <DevicesPanel />,
81
+ voice: () => <VoiceScreen />,
82
+ deck: () => <DeckScreen />,
83
+ desktop: () => <DesktopSettingsPanel />,
69
84
  appearance: () => <AppearancePanel />,
70
85
  advanced: () => <AdvancedPanel />,
71
86
  };
@@ -103,6 +118,9 @@ function tabFromPath(pathname: string): TabKey {
103
118
  case "skills": return "skills";
104
119
  case "telegram": return "telegram";
105
120
  case "devices": return "devices";
121
+ case "voice": return "voice";
122
+ case "deck": return "deck";
123
+ case "desktop": return "desktop";
106
124
  case "appearance": return "appearance";
107
125
  case "config":
108
126
  case "advanced": return "advanced";
@@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
3
3
  import useSWR from "swr";
4
4
  import { Tasks } from "../../lib/api";
5
5
  import { Section } from "../../components/Section";
6
+ import { Pager, usePaged } from "../../components/Pager";
6
7
  import { Badge, Button, Empty, Loading } from "../../components/ui";
7
8
  import { t } from "../../i18n";
8
9
 
@@ -11,6 +12,7 @@ export function GlobalTasksTab() {
11
12
  const navigate = useNavigate();
12
13
  const [state, setState] = useState<"open" | "done" | "dropped" | "all">("open");
13
14
  const list = useSWR(`/tasks?state=${state}`, () => Tasks.global(state));
15
+ const paged = usePaged(list.data || [], state);
14
16
 
15
17
  return (
16
18
  <Section
@@ -27,7 +29,7 @@ export function GlobalTasksTab() {
27
29
  {list.isLoading && <Loading />}
28
30
  {!list.isLoading && (list.data?.length ?? 0) === 0 && <Empty>{t("project.global_tasks.empty")}</Empty>}
29
31
  <ul className="space-y-2 text-sm">
30
- {(list.data || []).map((task) => (
32
+ {paged.slice.map((task) => (
31
33
  <li key={`${task.project_id}-${task.id}`} className="flex items-start gap-3 rounded-md border border-border bg-muted/30 px-3 py-2">
32
34
  <button
33
35
  type="button"
@@ -48,6 +50,7 @@ export function GlobalTasksTab() {
48
50
  </li>
49
51
  ))}
50
52
  </ul>
53
+ <Pager page={paged.page} pageCount={paged.pageCount} total={paged.total} start={paged.start} end={paged.end} pageSize={paged.pageSize} onPage={paged.setPage} onPageSize={paged.setPageSize} />
51
54
  </Section>
52
55
  );
53
56
  }