@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.
@@ -1,282 +1,32 @@
1
- import { useEffect, useMemo, useState } from "react";
2
- import { Link } from "react-router-dom";
1
+ import { useMemo } from "react";
3
2
  import useSWR from "swr";
4
- import { Section, Kbd, StatusDot } from "../../components/Section";
5
- import { Button, Field, Switch, Loading, Empty } from "../../components/ui";
6
- import { UiSelect } from "../../components/UiSelect";
7
- import { ShortcutInput } from "../../components/ShortcutInput";
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
- const DEFAULT_SHORTCUT = "CommandOrControl+G";
14
- const positionOpts = () => [
15
- { value: "left", label: t("modules_ui.desktop_pos_left") },
16
- { value: "center", label: t("modules_ui.desktop_pos_center") },
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-6xl space-y-6 p-6" data-testid="screen-desktop">
131
- {/* ── Two-column layout: config on the left, last conversation on the right. ── */}
132
- <div className="grid gap-6 xl:grid-cols-[1fr_1fr]">
133
- {/* ── LEFT: configuration + status ─────────────────────────────── */}
134
- <div className="space-y-6">
135
- <Section
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
- {/* ── RIGHT: last conversation preview ─────────────────────────── */}
29
+ {/* ── BOTTOM: last conversation preview ────────────────────────── */}
280
30
  <div>
281
31
  <Section
282
32
  title={t("desktop_screen.last_conv_title")}
@@ -120,7 +120,7 @@ export function VoiceScreen() {
120
120
  };
121
121
 
122
122
  return (
123
- <div className="p-6" data-testid="screen-voice">
123
+ <div data-testid="screen-voice">
124
124
  <div className="grid gap-6 xl:grid-cols-2">
125
125
  {/* Left: TTS providers */}
126
126
  <Section