@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.
- package/package.json +1 -1
- package/src/interfaces/web/dist/assets/{index-Cjj_d3SA.js → index-D7px5xcy.js} +120 -120
- package/src/interfaces/web/dist/assets/index-D7px5xcy.js.map +1 -0
- package/src/interfaces/web/dist/index.html +1 -1
- package/src/interfaces/web/src/App.tsx +3 -6
- package/src/interfaces/web/src/components/desktop/DesktopStatusCard.tsx +116 -0
- package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +7 -10
- package/src/interfaces/web/src/components/settings/DesktopSettingsPanel.tsx +185 -0
- package/src/interfaces/web/src/i18n/en.ts +2 -0
- package/src/interfaces/web/src/i18n/es.ts +2 -0
- package/src/interfaces/web/src/screens/SettingsScreen.tsx +21 -3
- package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +13 -263
- package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +1 -1
- package/src/interfaces/web/dist/assets/index-Cjj_d3SA.js.map +0 -1
|
@@ -1,279 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Link } from "react-router-dom";
|
|
1
|
+
import { useMemo } from "react";
|
|
3
2
|
import useSWR from "swr";
|
|
4
|
-
import { Section
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { useToast } from "../../components/Toast";
|
|
9
|
-
import { useGlobalConfig } from "../../hooks/useGlobalConfig";
|
|
10
|
-
import { Desktop, fetchDesktopMessages, type GlobalMessage } from "../../lib/api/desktop";
|
|
3
|
+
import { Section } from "../../components/Section";
|
|
4
|
+
import { Loading, Empty } from "../../components/ui";
|
|
5
|
+
import { DesktopStatusCard } from "../../components/desktop/DesktopStatusCard";
|
|
6
|
+
import { fetchDesktopMessages, type GlobalMessage } from "../../lib/api/desktop";
|
|
11
7
|
import { t } from "../../i18n";
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
{ value: "right", label: t("modules_ui.desktop_pos_right") },
|
|
18
|
-
];
|
|
19
|
-
const themeOpts = () => [
|
|
20
|
-
{ value: "system", label: t("modules_ui.desktop_theme_system") },
|
|
21
|
-
{ value: "light", label: t("modules_ui.desktop_theme_light") },
|
|
22
|
-
{ value: "dark", label: t("modules_ui.desktop_theme_dark") },
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
// Desktop module — manage the floating voice window (the Electron app launched
|
|
26
|
-
// with `apx desktop start`). The window is a separate process spawned by the
|
|
27
|
-
// CLI, so the web admin doesn't start/stop it — it edits persisted config,
|
|
28
|
-
// toggles per-user autostart, and previews the last conversation.
|
|
9
|
+
// Desktop module — the floating voice window (the Electron app launched with
|
|
10
|
+
// `apx desktop start`). This rail surface shows live status + lifecycle
|
|
11
|
+
// controls and the last conversation; all persisted settings (autostart,
|
|
12
|
+
// shortcut, appearance, activation) live in Settings → Desktop.
|
|
29
13
|
export function DesktopScreen() {
|
|
30
|
-
const toast = useToast();
|
|
31
|
-
const { config, isLoading: cfgLoading, patch } = useGlobalConfig();
|
|
32
|
-
|
|
33
|
-
// config.desktop isn't on the typed GlobalConfig — read it off a local view.
|
|
34
|
-
const cfgView = config as unknown as {
|
|
35
|
-
desktop?: {
|
|
36
|
-
shortcut?: string; enabled?: boolean;
|
|
37
|
-
theme?: "light" | "dark" | "system";
|
|
38
|
-
position?: "left" | "center" | "right";
|
|
39
|
-
};
|
|
40
|
-
overlay?: { shortcut?: string }; // legacy fallback
|
|
41
|
-
};
|
|
42
|
-
const savedShortcut = cfgView.desktop?.shortcut || cfgView.overlay?.shortcut || DEFAULT_SHORTCUT;
|
|
43
|
-
const enabled = cfgView.desktop?.enabled !== false;
|
|
44
|
-
// Default to "system" so the window follows the OS appearance until the
|
|
45
|
-
// user explicitly pins light/dark.
|
|
46
|
-
const theme = cfgView.desktop?.theme || "system";
|
|
47
|
-
const position = cfgView.desktop?.position || "right";
|
|
48
|
-
|
|
49
|
-
const { data: status, isLoading: stLoading, mutate: mutateStatus } = useSWR(
|
|
50
|
-
"/desktop/status",
|
|
51
|
-
() => Desktop.status(),
|
|
52
|
-
{ refreshInterval: 5000 },
|
|
53
|
-
);
|
|
54
|
-
const running = !!status?.running;
|
|
55
|
-
|
|
56
|
-
const { data: autostart, mutate: mutateAutostart } = useSWR(
|
|
57
|
-
"/desktop/autostart",
|
|
58
|
-
() => Desktop.autostartGet(),
|
|
59
|
-
);
|
|
60
|
-
|
|
61
14
|
const { data: msgs, isLoading: msgsLoading, mutate: mutateMsgs } = useSWR(
|
|
62
15
|
"/messages/global?channel=desktop",
|
|
63
16
|
() => fetchDesktopMessages(40),
|
|
64
17
|
{ refreshInterval: 8000 },
|
|
65
18
|
);
|
|
66
19
|
|
|
67
|
-
const [shortcut, setShortcut] = useState(savedShortcut);
|
|
68
|
-
const [busy, setBusy] = useState(false);
|
|
69
|
-
const [autostartBusy, setAutostartBusy] = useState(false);
|
|
70
|
-
// Which lifecycle action (start/stop/restart) is in flight — drives the
|
|
71
|
-
// per-button spinner and disables its siblings while one runs.
|
|
72
|
-
const [lifeAction, setLifeAction] = useState<"start" | "stop" | "restart" | null>(null);
|
|
73
|
-
useEffect(() => setShortcut(savedShortcut), [savedShortcut]);
|
|
74
|
-
|
|
75
|
-
// Start/Stop launch or kill the Electron window (daemon spawns/SIGTERMs it);
|
|
76
|
-
// Restart tells a live window to reload + re-read config (theme, position,
|
|
77
|
-
// shortcut) — the "apply now" the static status-poll never did. All three
|
|
78
|
-
// re-poll status shortly after so the dot + buttons settle.
|
|
79
|
-
const runLifecycle = async (action: "start" | "stop" | "restart", fn: () => Promise<void>) => {
|
|
80
|
-
setLifeAction(action);
|
|
81
|
-
try { await fn(); }
|
|
82
|
-
catch (e) { toast.error((e as Error).message); }
|
|
83
|
-
finally { setLifeAction(null); setTimeout(() => mutateStatus(), 1200); }
|
|
84
|
-
};
|
|
85
|
-
const startDesktop = () => runLifecycle("start", async () => {
|
|
86
|
-
const r = await Desktop.start();
|
|
87
|
-
toast.success(r.already ? t("modules_ui.desktop_start_already") : t("modules_ui.desktop_start_done"));
|
|
88
|
-
});
|
|
89
|
-
const stopDesktop = () => runLifecycle("stop", async () => {
|
|
90
|
-
const r = await Desktop.stop();
|
|
91
|
-
toast.success(r.stopped ? t("modules_ui.desktop_stop_done") : t("modules_ui.desktop_stop_none"));
|
|
92
|
-
});
|
|
93
|
-
const restartDesktop = () => runLifecycle("restart", async () => {
|
|
94
|
-
const r = await Desktop.restart();
|
|
95
|
-
if (r.reloaded > 0) toast.success(t("modules_ui.desktop_restart_done"));
|
|
96
|
-
else toast.info(t("modules_ui.desktop_restart_none"));
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
const saveShortcut = async () => {
|
|
100
|
-
const next = shortcut.trim();
|
|
101
|
-
if (!next || next === savedShortcut) return;
|
|
102
|
-
setBusy(true);
|
|
103
|
-
try {
|
|
104
|
-
await patch({ "desktop.shortcut": next });
|
|
105
|
-
toast.success(t("modules_ui.desktop_shortcut_saved"));
|
|
106
|
-
} catch (e) { toast.error((e as Error).message); }
|
|
107
|
-
finally { setBusy(false); }
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const patchKey = async (key: string, value: unknown, ok: string) => {
|
|
111
|
-
setBusy(true);
|
|
112
|
-
try {
|
|
113
|
-
await patch({ [key]: value });
|
|
114
|
-
toast.success(ok);
|
|
115
|
-
} catch (e) { toast.error((e as Error).message); }
|
|
116
|
-
finally { setBusy(false); }
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
const toggleAutostart = async (v: boolean) => {
|
|
120
|
-
setAutostartBusy(true);
|
|
121
|
-
try {
|
|
122
|
-
await Desktop.autostartSet(v);
|
|
123
|
-
await mutateAutostart();
|
|
124
|
-
toast.success(v ? t("modules_ui.desktop_autostart_on") : t("modules_ui.desktop_autostart_off"));
|
|
125
|
-
} catch (e) { toast.error((e as Error).message); }
|
|
126
|
-
finally { setAutostartBusy(false); }
|
|
127
|
-
};
|
|
128
|
-
|
|
129
20
|
return (
|
|
130
21
|
<div className="mx-auto max-w-6xl space-y-6 p-6" data-testid="screen-desktop">
|
|
131
|
-
{/* ── Two-column layout:
|
|
22
|
+
{/* ── Two-column layout: status on the left, last conversation on the right. ── */}
|
|
132
23
|
<div className="grid gap-6 xl:grid-cols-[1fr_1fr]">
|
|
133
|
-
{/* ── LEFT:
|
|
134
|
-
<div
|
|
135
|
-
<
|
|
136
|
-
title={t("desktop_screen.status_title")}
|
|
137
|
-
description={t("modules_ui.desktop_status_desc")}
|
|
138
|
-
action={
|
|
139
|
-
<div className="flex items-center gap-2">
|
|
140
|
-
<Button
|
|
141
|
-
variant="primary"
|
|
142
|
-
size="sm"
|
|
143
|
-
onClick={startDesktop}
|
|
144
|
-
loading={lifeAction === "start"}
|
|
145
|
-
disabled={running || (lifeAction !== null && lifeAction !== "start")}
|
|
146
|
-
>
|
|
147
|
-
{t("modules_ui.desktop_start")}
|
|
148
|
-
</Button>
|
|
149
|
-
<Button
|
|
150
|
-
variant="secondary"
|
|
151
|
-
size="sm"
|
|
152
|
-
onClick={stopDesktop}
|
|
153
|
-
loading={lifeAction === "stop"}
|
|
154
|
-
disabled={!running || (lifeAction !== null && lifeAction !== "stop")}
|
|
155
|
-
>
|
|
156
|
-
{t("modules_ui.desktop_stop")}
|
|
157
|
-
</Button>
|
|
158
|
-
<Button
|
|
159
|
-
variant="secondary"
|
|
160
|
-
size="sm"
|
|
161
|
-
onClick={restartDesktop}
|
|
162
|
-
loading={lifeAction === "restart"}
|
|
163
|
-
disabled={!running || (lifeAction !== null && lifeAction !== "restart")}
|
|
164
|
-
title={t("modules_ui.desktop_restart_hint")}
|
|
165
|
-
>
|
|
166
|
-
{t("modules_ui.desktop_restart")}
|
|
167
|
-
</Button>
|
|
168
|
-
</div>
|
|
169
|
-
}
|
|
170
|
-
>
|
|
171
|
-
{stLoading ? <Loading /> : (
|
|
172
|
-
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm">
|
|
173
|
-
<StatusDot ok={running} />
|
|
174
|
-
<span className="font-medium">{running ? t("modules_ui.desktop_running") : t("modules_ui.desktop_stopped")}</span>
|
|
175
|
-
<button
|
|
176
|
-
type="button"
|
|
177
|
-
onClick={() => mutateStatus()}
|
|
178
|
-
className="text-xs text-muted-fg underline-offset-2 hover:underline"
|
|
179
|
-
>
|
|
180
|
-
{t("modules_ui.desktop_refresh")}
|
|
181
|
-
</button>
|
|
182
|
-
<span className="text-xs text-muted-fg">
|
|
183
|
-
({t("modules_ui.desktop_from_terminal")} <Kbd>apx desktop start</Kbd> · <Kbd>apx desktop --debug</Kbd>)
|
|
184
|
-
</span>
|
|
185
|
-
</div>
|
|
186
|
-
)}
|
|
187
|
-
</Section>
|
|
188
|
-
|
|
189
|
-
<Section
|
|
190
|
-
title={t("desktop_screen.autostart_title")}
|
|
191
|
-
description={t("modules_ui.desktop_autostart_desc")}
|
|
192
|
-
>
|
|
193
|
-
{!autostart ? <Loading /> : (
|
|
194
|
-
<div className="flex items-center justify-between gap-3">
|
|
195
|
-
<Switch
|
|
196
|
-
checked={autostart.enabled}
|
|
197
|
-
onChange={toggleAutostart}
|
|
198
|
-
disabled={autostartBusy}
|
|
199
|
-
label={autostart.enabled ? t("common.enabled") : t("common.disabled")}
|
|
200
|
-
/>
|
|
201
|
-
<span className="text-xs text-muted-fg">{t("modules_ui.desktop_platform", { platform: autostart.platform })}</span>
|
|
202
|
-
</div>
|
|
203
|
-
)}
|
|
204
|
-
</Section>
|
|
205
|
-
|
|
206
|
-
<Section
|
|
207
|
-
title={t("desktop_screen.shortcut_title")}
|
|
208
|
-
description={t("modules_ui.desktop_shortcut_desc")}
|
|
209
|
-
>
|
|
210
|
-
{cfgLoading ? <Loading /> : (
|
|
211
|
-
<Field
|
|
212
|
-
label={t("modules_ui.desktop_accelerator")}
|
|
213
|
-
hint={t("modules_ui.desktop_accelerator_hint")}
|
|
214
|
-
>
|
|
215
|
-
<ShortcutInput
|
|
216
|
-
value={shortcut}
|
|
217
|
-
onChange={setShortcut}
|
|
218
|
-
disabled={busy}
|
|
219
|
-
trailing={
|
|
220
|
-
<Button
|
|
221
|
-
variant="primary"
|
|
222
|
-
size="sm"
|
|
223
|
-
onClick={saveShortcut}
|
|
224
|
-
loading={busy}
|
|
225
|
-
disabled={!shortcut.trim() || shortcut.trim() === savedShortcut}
|
|
226
|
-
>
|
|
227
|
-
{t("common.save")}
|
|
228
|
-
</Button>
|
|
229
|
-
}
|
|
230
|
-
/>
|
|
231
|
-
</Field>
|
|
232
|
-
)}
|
|
233
|
-
</Section>
|
|
234
|
-
|
|
235
|
-
<Section title={t("desktop_screen.appearance_title")} description={t("modules_ui.desktop_appearance_desc")}>
|
|
236
|
-
{cfgLoading ? <Loading /> : (
|
|
237
|
-
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
238
|
-
<Field label={t("modules_ui.desktop_theme")} hint={t("modules_ui.desktop_restart_apply")}>
|
|
239
|
-
<UiSelect
|
|
240
|
-
value={theme}
|
|
241
|
-
onChange={(v) => patchKey("desktop.theme", v, t("modules_ui.desktop_theme_set", { value: v }))}
|
|
242
|
-
options={themeOpts()}
|
|
243
|
-
disabled={busy}
|
|
244
|
-
/>
|
|
245
|
-
</Field>
|
|
246
|
-
<Field label={t("modules_ui.desktop_position")} hint={t("modules_ui.desktop_position_hint")}>
|
|
247
|
-
<UiSelect
|
|
248
|
-
value={position}
|
|
249
|
-
onChange={(v) => patchKey("desktop.position", v, t("modules_ui.desktop_position_set", { value: v }))}
|
|
250
|
-
options={positionOpts()}
|
|
251
|
-
disabled={busy}
|
|
252
|
-
/>
|
|
253
|
-
</Field>
|
|
254
|
-
</div>
|
|
255
|
-
)}
|
|
256
|
-
</Section>
|
|
257
|
-
|
|
258
|
-
<Section
|
|
259
|
-
title={t("desktop_screen.activation_title")}
|
|
260
|
-
description={t("modules_ui.desktop_activation_desc")}
|
|
261
|
-
>
|
|
262
|
-
{cfgLoading ? <Loading /> : (
|
|
263
|
-
<div className="space-y-3">
|
|
264
|
-
<Switch
|
|
265
|
-
checked={enabled}
|
|
266
|
-
onChange={(v) => patchKey("desktop.enabled", v, v ? t("modules_ui.desktop_enabled_toast") : t("modules_ui.desktop_disabled_toast"))}
|
|
267
|
-
disabled={busy}
|
|
268
|
-
label={enabled ? t("modules_ui.desktop_plugin_on") : t("modules_ui.desktop_plugin_off")}
|
|
269
|
-
/>
|
|
270
|
-
<p className="text-xs text-muted-fg">
|
|
271
|
-
{t("modules_ui.desktop_stt_engine")} <Link to="/m/voice" className="font-medium text-fg underline underline-offset-2">{t("nav.modules.voice")}</Link>{" "}
|
|
272
|
-
{t("modules_ui.desktop_stt_engine_suffix")}
|
|
273
|
-
</p>
|
|
274
|
-
</div>
|
|
275
|
-
)}
|
|
276
|
-
</Section>
|
|
24
|
+
{/* ── LEFT: live status + lifecycle + link to configuration ─────── */}
|
|
25
|
+
<div>
|
|
26
|
+
<DesktopStatusCard showConfigLink />
|
|
277
27
|
</div>
|
|
278
28
|
|
|
279
29
|
{/* ── RIGHT: last conversation preview ─────────────────────────── */}
|