@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.
- package/package.json +1 -1
- package/src/interfaces/web/dist/assets/index-BAKk7d_M.css +1 -0
- package/src/interfaces/web/dist/assets/index-D7px5xcy.js +656 -0
- package/src/interfaces/web/dist/assets/index-D7px5xcy.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/Pager.tsx +88 -0
- 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 +7 -0
- package/src/interfaces/web/src/i18n/es.ts +7 -0
- package/src/interfaces/web/src/screens/SettingsScreen.tsx +21 -3
- package/src/interfaces/web/src/screens/base/GlobalTasksTab.tsx +4 -1
- package/src/interfaces/web/src/screens/base/SessionsTab.tsx +4 -1
- 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/src/screens/project/TasksTab.tsx +4 -1
- package/src/interfaces/web/dist/assets/index-B0nTYflm.js +0 -651
- package/src/interfaces/web/dist/assets/index-B0nTYflm.js.map +0 -1
- package/src/interfaces/web/dist/assets/index-C22PmKCD.css +0 -1
|
@@ -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-
|
|
22
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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
|
|
2
|
-
// rail-level MODULES (
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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,
|
|
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: "
|
|
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
|
|
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"
|
|
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
|
-
{
|
|
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
|
}
|