@agentprojectcontext/apx 1.44.0 → 1.46.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-BAKk7d_M.css → index-CilEtMjV.css} +1 -1
- package/src/interfaces/web/dist/assets/{index-Cjj_d3SA.js → index-oOjZZktw.js} +120 -120
- package/src/interfaces/web/dist/assets/index-oOjZZktw.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- 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 +16 -266
- 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,282 +1,32 @@
|
|
|
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
|
-
<div className="mx-auto max-w-
|
|
131
|
-
{/* ──
|
|
132
|
-
<div className="
|
|
133
|
-
{/* ──
|
|
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>
|
|
21
|
+
<div className="mx-auto max-w-3xl space-y-6 p-6" data-testid="screen-desktop">
|
|
22
|
+
{/* ── Single-column layout: status on top, conversation list below. ── */}
|
|
23
|
+
<div className="space-y-6">
|
|
24
|
+
{/* ── TOP: live status + lifecycle + link to configuration ─────── */}
|
|
25
|
+
<div>
|
|
26
|
+
<DesktopStatusCard showConfigLink />
|
|
277
27
|
</div>
|
|
278
28
|
|
|
279
|
-
{/* ──
|
|
29
|
+
{/* ── BOTTOM: last conversation preview ────────────────────────── */}
|
|
280
30
|
<div>
|
|
281
31
|
<Section
|
|
282
32
|
title={t("desktop_screen.last_conv_title")}
|