@agentprojectcontext/apx 1.44.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,7 +18,7 @@
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-Cjj_d3SA.js"></script>
21
+ <script type="module" crossorigin src="/assets/index-D7px5xcy.js"></script>
22
22
  <link rel="stylesheet" crossorigin href="/assets/index-BAKk7d_M.css">
23
23
  </head>
24
24
  <body class="bg-background text-foreground antialiased">
@@ -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,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
+ }
@@ -180,6 +180,7 @@ export const en = {
180
180
  account_section: "Account",
181
181
  agents_section: "Agents & models",
182
182
  channels_section: "Channels & devices",
183
+ modules_section: "Modules",
183
184
  advanced_section: "Advanced",
184
185
 
185
186
  tabs: {
@@ -930,6 +931,7 @@ export const en = {
930
931
  appearance_title: "Appearance",
931
932
  activation_title: "Activation + transcription",
932
933
  last_conv_title: "Last conversation",
934
+ open_config: "Configuration",
933
935
  },
934
936
 
935
937
  voice_screen: {
@@ -181,6 +181,7 @@ export const es = {
181
181
  account_section: "Cuenta",
182
182
  agents_section: "Agentes & modelos",
183
183
  channels_section: "Canales & dispositivos",
184
+ modules_section: "Módulos",
184
185
  advanced_section: "Avanzado",
185
186
 
186
187
  tabs: {
@@ -928,6 +929,7 @@ export const es = {
928
929
  appearance_title: "Apariencia",
929
930
  activation_title: "Activación + transcripción",
930
931
  last_conv_title: "Última conversación",
932
+ open_config: "Configuración",
931
933
  },
932
934
 
933
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";