Merge PR #4330: feat(webui): add automation management view
feat(webui): add automation management view
This commit is contained in:
+11
-60
@@ -1,6 +1,10 @@
|
||||
# nanobot WebUI
|
||||
# nanobot WebUI Source
|
||||
|
||||
The WebUI is the browser workbench served by `nanobot gateway`. If you installed `nanobot-ai` from PyPI, the WebUI bundle is already included; this `webui/` source tree is only needed when you are changing the frontend.
|
||||
This directory contains the React/TypeScript source for the nanobot WebUI. If
|
||||
you installed `nanobot-ai` from PyPI and only want to use the bundled browser UI,
|
||||
read the user guide in [`docs/webui.md`](../docs/webui.md). You do not need
|
||||
Node.js, Bun, Vite, or anything in this directory unless you are changing the
|
||||
frontend.
|
||||
|
||||
For the project overview, install guide, and general docs map, see the root [`README.md`](../README.md) and [`docs/README.md`](../docs/README.md).
|
||||
|
||||
@@ -8,46 +12,14 @@ For the project overview, install guide, and general docs map, see the root [`RE
|
||||
|
||||
| Goal | Start with | Opens at |
|
||||
|---|---|---|
|
||||
| Use the bundled browser UI | [Just want to use the WebUI?](#just-want-to-use-the-webui) | `http://127.0.0.1:8765` |
|
||||
| Use the WebUI from another device | [Access from another device (LAN)](#access-from-another-device-lan) | `http://<your-ip>:8765` |
|
||||
| Use the bundled browser UI | [`docs/webui.md`](../docs/webui.md) | `http://127.0.0.1:8765` |
|
||||
| Use the WebUI from another device | [`docs/webui.md#lan-access`](../docs/webui.md#lan-access) | `http://<your-ip>:8765` |
|
||||
| Change WebUI source code | [Develop the WebUI (Vite HMR)](#develop-the-webui-vite-hmr) | `http://127.0.0.1:5173` |
|
||||
| Debug setup failures | [`docs/troubleshooting.md#webui-problems`](../docs/troubleshooting.md#webui-problems) | Diagnosis order and common fixes |
|
||||
|
||||
## Just want to use the WebUI?
|
||||
|
||||
If you installed nanobot via `python -m pip install nanobot-ai`, the WebUI is **already bundled** in the wheel. You do **not** need Node.js, Bun, Vite, or anything in this directory unless you are changing the WebUI source code.
|
||||
|
||||
First prove the provider path:
|
||||
|
||||
```bash
|
||||
nanobot agent -m "Hello!"
|
||||
```
|
||||
|
||||
If the shell cannot find `nanobot`, use the module form from the same Python environment:
|
||||
|
||||
```bash
|
||||
python -m nanobot agent -m "Hello!"
|
||||
```
|
||||
|
||||
Then merge this WebSocket snippet into your existing `~/.nanobot/config.json` instead of replacing the whole file:
|
||||
|
||||
```json
|
||||
{ "channels": { "websocket": { "enabled": true } } }
|
||||
```
|
||||
|
||||
If you are new to JSON snippets, see [`docs/start-without-technical-background.md#how-to-merge-json-snippets`](../docs/start-without-technical-background.md#how-to-merge-json-snippets).
|
||||
|
||||
Start the gateway:
|
||||
|
||||
```bash
|
||||
nanobot gateway
|
||||
```
|
||||
|
||||
Leave this terminal running while you use the WebUI. Closing it stops the browser UI and WebSocket connection.
|
||||
|
||||
Open [`http://127.0.0.1:8765`](http://127.0.0.1:8765). The gateway's `18790` port is only the health endpoint, not the browser UI. For setup failures, use [`docs/troubleshooting.md`](../docs/troubleshooting.md#webui-problems).
|
||||
|
||||
This `webui/` tree is for people **changing the WebUI source code**. It is built with Vite + React 18 + TypeScript + Tailwind 3 + shadcn/ui, talks to the gateway over the WebSocket multiplex protocol, and reads session metadata from the embedded REST surface on the same port.
|
||||
The source app is built with Vite + React 18 + TypeScript + Tailwind 3 +
|
||||
shadcn/ui. It talks to the gateway over the WebSocket multiplex protocol and
|
||||
reads session metadata from the embedded REST surface on the same port.
|
||||
|
||||
## Layout
|
||||
|
||||
@@ -104,27 +76,6 @@ If your gateway listens on a non-default port, point the dev server at it:
|
||||
NANOBOT_API_URL=http://127.0.0.1:9000 bun run dev
|
||||
```
|
||||
|
||||
### Access from another device (LAN)
|
||||
|
||||
To use the WebUI from another device on the same network, set `host` to `"0.0.0.0"` and configure a `token` or `tokenIssueSecret` in `~/.nanobot/config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"websocket": {
|
||||
"enabled": true,
|
||||
"host": "0.0.0.0",
|
||||
"port": 8765,
|
||||
"tokenIssueSecret": "your-secret-here"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The gateway will refuse to start if `host` is `"0.0.0.0"` and neither `token` nor `tokenIssueSecret` is set.
|
||||
|
||||
Then open `http://<your-ip>:8765` on the other device. The WebUI will show an authentication form where you enter the secret. It is saved in your browser so you only need to enter it once.
|
||||
|
||||
## Build for packaged runtime
|
||||
|
||||
You usually do not need to run this by hand: `python -m build` invokes the WebUI build automatically when packaging the wheel.
|
||||
|
||||
+54
-21
@@ -65,14 +65,15 @@ type BootState =
|
||||
};
|
||||
|
||||
const SIDEBAR_STORAGE_KEY = "nanobot-webui.sidebar";
|
||||
const COMPLETED_RUNS_STORAGE_KEY = "nanobot-webui.sidebar.completed-runs.v1";
|
||||
const SESSION_UPDATES_STORAGE_KEY = "nanobot-webui.sidebar.session-updates.v1";
|
||||
const LEGACY_COMPLETED_RUNS_STORAGE_KEY = "nanobot-webui.sidebar.completed-runs.v1";
|
||||
const RESTART_STARTED_KEY = "nanobot-webui.restartStartedAt";
|
||||
const SIDEBAR_WIDTH = 272;
|
||||
const SIDEBAR_RAIL_WIDTH = 56;
|
||||
const MOBILE_SIDEBAR_WIDTH = `min(${SIDEBAR_WIDTH}px, calc(100vw - 0.75rem))`;
|
||||
const TOKEN_REFRESH_MARGIN_MS = 30_000;
|
||||
const TOKEN_REFRESH_MIN_DELAY_MS = 5_000;
|
||||
type ShellView = "chat" | "settings" | "apps" | "skills";
|
||||
type ShellView = "chat" | "settings" | "apps" | "automations" | "skills";
|
||||
type ShellRoute = {
|
||||
view: ShellView;
|
||||
activeKey: string | null;
|
||||
@@ -87,6 +88,7 @@ const SETTINGS_SECTION_KEYS: SettingsSectionKey[] = [
|
||||
"voice",
|
||||
"browser",
|
||||
"apps",
|
||||
"automations",
|
||||
"skills",
|
||||
"runtime",
|
||||
"advanced",
|
||||
@@ -101,7 +103,7 @@ function defaultShellRoute(): ShellRoute {
|
||||
}
|
||||
|
||||
function shellViewForSettingsSection(section: SettingsSectionKey): ShellView {
|
||||
if (section === "apps" || section === "skills") return section;
|
||||
if (section === "apps" || section === "automations" || section === "skills") return section;
|
||||
return "settings";
|
||||
}
|
||||
|
||||
@@ -130,6 +132,9 @@ function readShellRoute(): ShellRoute {
|
||||
if (path === "/apps") {
|
||||
return { view: "apps", activeKey, settingsSection: "apps" };
|
||||
}
|
||||
if (path === "/automations") {
|
||||
return { view: "automations", activeKey, settingsSection: "automations" };
|
||||
}
|
||||
if (path === "/skills") {
|
||||
return { view: "skills", activeKey, settingsSection: "skills" };
|
||||
}
|
||||
@@ -255,10 +260,12 @@ function readSidebarOpen(): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
function readCompletedRunChatIds(): Set<string> {
|
||||
function readSessionUpdateChatIds(): Set<string> {
|
||||
if (typeof window === "undefined") return new Set();
|
||||
try {
|
||||
const raw = window.localStorage.getItem(COMPLETED_RUNS_STORAGE_KEY);
|
||||
const raw =
|
||||
window.localStorage.getItem(SESSION_UPDATES_STORAGE_KEY)
|
||||
?? window.localStorage.getItem(LEGACY_COMPLETED_RUNS_STORAGE_KEY);
|
||||
const parsed = raw ? JSON.parse(raw) : [];
|
||||
if (!Array.isArray(parsed)) return new Set();
|
||||
return new Set(parsed.filter((item): item is string => typeof item === "string"));
|
||||
@@ -267,10 +274,10 @@ function readCompletedRunChatIds(): Set<string> {
|
||||
}
|
||||
}
|
||||
|
||||
function writeCompletedRunChatIds(chatIds: Set<string>): void {
|
||||
function writeSessionUpdateChatIds(chatIds: Set<string>): void {
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
COMPLETED_RUNS_STORAGE_KEY,
|
||||
SESSION_UPDATES_STORAGE_KEY,
|
||||
JSON.stringify(Array.from(chatIds)),
|
||||
);
|
||||
} catch {
|
||||
@@ -570,7 +577,7 @@ function Shell({
|
||||
const [restartToast, setRestartToast] = useState<string | null>(null);
|
||||
const [isRestarting, setIsRestarting] = useState(false);
|
||||
const [runningChatIds, setRunningChatIds] = useState<Set<string>>(() => new Set());
|
||||
const [completedChatIds, setCompletedChatIds] = useState<Set<string>>(readCompletedRunChatIds);
|
||||
const [updatedChatIds, setUpdatedChatIds] = useState<Set<string>>(readSessionUpdateChatIds);
|
||||
const [workspaces, setWorkspaces] = useState<WorkspacesPayload | null>(null);
|
||||
const skills = useSkills(token);
|
||||
const [settingsSnapshot, setSettingsSnapshot] = useState<SettingsPayload | null>(null);
|
||||
@@ -638,20 +645,20 @@ function Shell({
|
||||
}, [hostSidebarOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
writeCompletedRunChatIds(completedChatIds);
|
||||
}, [completedChatIds]);
|
||||
writeSessionUpdateChatIds(updatedChatIds);
|
||||
}, [updatedChatIds]);
|
||||
|
||||
const activeSession = useMemo<ChatSummary | null>(() => {
|
||||
if (!activeKey) return null;
|
||||
return sessions.find((s) => s.key === activeKey) ?? null;
|
||||
}, [sessions, activeKey]);
|
||||
const runningChatIdList = useMemo(() => Array.from(runningChatIds), [runningChatIds]);
|
||||
const completedChatIdList = useMemo(() => Array.from(completedChatIds), [completedChatIds]);
|
||||
const updatedChatIdList = useMemo(() => Array.from(updatedChatIds), [updatedChatIds]);
|
||||
const activeChatId = activeSession?.chatId ?? null;
|
||||
useEffect(() => {
|
||||
activeChatIdRef.current = activeChatId;
|
||||
if (!activeChatId) return;
|
||||
setCompletedChatIds((current) => {
|
||||
setUpdatedChatIds((current) => {
|
||||
if (!current.has(activeChatId)) return current;
|
||||
const next = new Set(current);
|
||||
next.delete(activeChatId);
|
||||
@@ -691,7 +698,7 @@ function Shell({
|
||||
useEffect(() => {
|
||||
if (loading) return;
|
||||
const knownChatIds = new Set(sessions.map((session) => session.chatId));
|
||||
setCompletedChatIds((current) => {
|
||||
setUpdatedChatIds((current) => {
|
||||
const next = new Set(
|
||||
Array.from(current).filter((chatId) => knownChatIds.has(chatId)),
|
||||
);
|
||||
@@ -719,12 +726,25 @@ function Shell({
|
||||
}, [activeKey, loading, navigate, sessions]);
|
||||
|
||||
useEffect(() => {
|
||||
return client.onSessionUpdate((_chatId, _scope, workspaceScope) => {
|
||||
return client.onSessionUpdate((chatId, scope, workspaceScope) => {
|
||||
if (scope === "thread") {
|
||||
setUpdatedChatIds((current) => {
|
||||
const next = new Set(current);
|
||||
if (activeChatIdRef.current === chatId) {
|
||||
next.delete(chatId);
|
||||
} else {
|
||||
next.add(chatId);
|
||||
}
|
||||
return next.size === current.size && next.has(chatId) === current.has(chatId)
|
||||
? current
|
||||
: next;
|
||||
});
|
||||
}
|
||||
if (!workspaceScope) return;
|
||||
const next = normalizeWorkspaceScope(workspaceScope);
|
||||
setWorkspaceOverrides((current) => ({
|
||||
...current,
|
||||
[_chatId]: next,
|
||||
[chatId]: next,
|
||||
}));
|
||||
setDraftWorkspaceScope(next);
|
||||
setWorkspaceError(null);
|
||||
@@ -761,7 +781,7 @@ function Shell({
|
||||
runningChatIdsRef.current = next;
|
||||
return next;
|
||||
});
|
||||
setCompletedChatIds((current) => {
|
||||
setUpdatedChatIds((current) => {
|
||||
let changed = false;
|
||||
const next = new Set(current);
|
||||
for (const chatId of activeRunIds) {
|
||||
@@ -958,7 +978,7 @@ function Shell({
|
||||
const selected = sessions.find((session) => session.key === key);
|
||||
const selectedChatId = selected?.chatId;
|
||||
if (selectedChatId) {
|
||||
setCompletedChatIds((current) => {
|
||||
setUpdatedChatIds((current) => {
|
||||
if (!current.has(selectedChatId)) return current;
|
||||
const next = new Set(current);
|
||||
next.delete(selectedChatId);
|
||||
@@ -1166,6 +1186,12 @@ function Shell({
|
||||
setMobileSidebarOpen(false);
|
||||
}, [activeKey, navigate]);
|
||||
|
||||
const onOpenAutomations = useCallback(() => {
|
||||
setSessionSearchOpen(false);
|
||||
navigate({ view: "automations", activeKey, settingsSection: "automations" });
|
||||
setMobileSidebarOpen(false);
|
||||
}, [activeKey, navigate]);
|
||||
|
||||
const onOpenSkills = useCallback(() => {
|
||||
setSessionSearchOpen(false);
|
||||
navigate({ view: "skills", activeKey, settingsSection: "skills" });
|
||||
@@ -1223,7 +1249,7 @@ function Shell({
|
||||
nextRunning.add(chatId);
|
||||
runningChatIdsRef.current = nextRunning;
|
||||
setRunningChatIds(nextRunning);
|
||||
setCompletedChatIds((current) => {
|
||||
setUpdatedChatIds((current) => {
|
||||
if (!current.has(chatId)) return current;
|
||||
const next = new Set(current);
|
||||
next.delete(chatId);
|
||||
@@ -1237,7 +1263,7 @@ function Shell({
|
||||
nextRunning.delete(chatId);
|
||||
runningChatIdsRef.current = nextRunning;
|
||||
setRunningChatIds(nextRunning);
|
||||
setCompletedChatIds((current) => {
|
||||
setUpdatedChatIds((current) => {
|
||||
const next = new Set(current);
|
||||
if (activeChatIdRef.current === chatId) {
|
||||
next.delete(chatId);
|
||||
@@ -1341,6 +1367,12 @@ function Shell({
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (view === "automations") {
|
||||
document.title = t("app.documentTitle.chat", {
|
||||
title: t("settings.nav.automations", { defaultValue: "Automations" }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (view === "skills") {
|
||||
document.title = t("app.documentTitle.chat", {
|
||||
title: t("settings.nav.skills", { defaultValue: "Skills" }),
|
||||
@@ -1367,9 +1399,10 @@ function Shell({
|
||||
onNewChatInProject,
|
||||
onOpenSettings,
|
||||
onOpenApps,
|
||||
onOpenAutomations,
|
||||
onOpenSkills,
|
||||
onOpenSearch: onOpenSessionSearch,
|
||||
activeUtility: view === "apps" || view === "skills" ? view : null,
|
||||
activeUtility: view === "apps" || view === "automations" || view === "skills" ? view : null,
|
||||
onToggleArchived,
|
||||
pinnedKeys: sidebarState.pinned_keys,
|
||||
archivedKeys: sidebarState.archived_keys,
|
||||
@@ -1377,7 +1410,7 @@ function Shell({
|
||||
projectNameOverrides: sidebarState.project_name_overrides,
|
||||
collapsedGroups: sidebarState.collapsed_groups,
|
||||
runningChatIds: runningChatIdList,
|
||||
completedChatIds: completedChatIdList,
|
||||
updatedChatIds: updatedChatIdList,
|
||||
viewState: sidebarState.view,
|
||||
showArchived: sidebarState.view.show_archived,
|
||||
archivedCount: sidebarState.archived_keys.length,
|
||||
|
||||
@@ -60,7 +60,7 @@ interface ChatListProps {
|
||||
projectNameOverrides?: Record<string, string>;
|
||||
collapsedGroups?: Record<string, boolean>;
|
||||
runningChatIds?: string[];
|
||||
completedChatIds?: string[];
|
||||
updatedChatIds?: string[];
|
||||
density?: SidebarDensity;
|
||||
showPreviews?: boolean;
|
||||
showTimestamps?: boolean;
|
||||
@@ -89,7 +89,7 @@ export const ChatList = memo(function ChatList({
|
||||
projectNameOverrides = {},
|
||||
collapsedGroups = {},
|
||||
runningChatIds = [],
|
||||
completedChatIds = [],
|
||||
updatedChatIds = [],
|
||||
density = "comfortable",
|
||||
showPreviews = false,
|
||||
showTimestamps = false,
|
||||
@@ -175,7 +175,7 @@ export const ChatList = memo(function ChatList({
|
||||
const pinned = new Set(pinnedKeys);
|
||||
const archived = new Set(archivedKeys);
|
||||
const running = new Set(runningChatIds);
|
||||
const completed = new Set(completedChatIds);
|
||||
const updated = new Set(updatedChatIds);
|
||||
const compact = density === "compact";
|
||||
const firstProjectGroupIndex = limitedGroups.findIndex((group) => group.kind === "project");
|
||||
|
||||
@@ -245,8 +245,8 @@ export const ChatList = memo(function ChatList({
|
||||
const projectMode = group.kind === "project";
|
||||
const activityState = running.has(s.chatId)
|
||||
? "running"
|
||||
: completed.has(s.chatId) && !active
|
||||
? "complete"
|
||||
: updated.has(s.chatId) && !active
|
||||
? "updated"
|
||||
: null;
|
||||
return (
|
||||
<li key={s.key} className="min-w-0">
|
||||
@@ -525,7 +525,7 @@ function ChatsFoldFooter({
|
||||
function SessionActivityIndicator({
|
||||
state,
|
||||
}: {
|
||||
state: "running" | "complete" | null;
|
||||
state: "running" | "updated" | null;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -542,15 +542,15 @@ function SessionActivityIndicator({
|
||||
);
|
||||
}
|
||||
|
||||
if (state === "complete") {
|
||||
const label = t("chat.activity.complete");
|
||||
if (state === "updated") {
|
||||
const label = t("chat.activity.updated");
|
||||
return (
|
||||
<span
|
||||
aria-label={label}
|
||||
title={label}
|
||||
className="grid h-4 w-4 shrink-0 place-items-center"
|
||||
>
|
||||
<span className="h-2 w-2 rounded-full bg-blue-500 dark:bg-blue-400" />
|
||||
<span className="h-2 w-2 rounded-full bg-[#ff8a3d] shadow-[0_0_0_2px_rgba(255,138,61,0.16)]" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,16 +80,16 @@ export function DeleteConfirm({
|
||||
</div>
|
||||
) : null}
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter className="mt-7 grid grid-cols-1 gap-3 space-x-0 sm:grid-cols-2">
|
||||
<AlertDialogFooter className="mt-7 !grid grid-cols-1 gap-3 space-x-0 sm:grid-cols-2 sm:space-x-0">
|
||||
<AlertDialogCancel
|
||||
onClick={onCancel}
|
||||
className="mt-0 h-11 rounded-full border-0 bg-muted/70 px-5 text-[15px] font-semibold text-foreground shadow-none hover:bg-muted"
|
||||
className="mt-0 h-11 w-full min-w-0 rounded-full border-0 bg-muted/70 px-5 text-[15px] font-semibold text-foreground shadow-none hover:bg-muted"
|
||||
>
|
||||
{t("deleteConfirm.cancel")}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={onConfirm}
|
||||
className="h-11 rounded-full bg-destructive px-5 text-[15px] font-semibold text-destructive-foreground shadow-[0_10px_25px_rgba(239,68,68,0.28)] hover:bg-destructive/90"
|
||||
className="h-11 w-full min-w-0 !whitespace-normal rounded-full bg-destructive px-5 text-center text-[15px] font-semibold text-destructive-foreground shadow-[0_10px_25px_rgba(239,68,68,0.28)] hover:bg-destructive/90"
|
||||
>
|
||||
{hasAutomations
|
||||
? t("deleteConfirm.confirmWithAutomations")
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, type ReactNode } from "react";
|
||||
import {
|
||||
Archive,
|
||||
Brain,
|
||||
CalendarClock,
|
||||
Menu,
|
||||
Search,
|
||||
Settings,
|
||||
@@ -36,8 +37,9 @@ interface SidebarProps {
|
||||
onOpenSettings: () => void;
|
||||
onOpenApps: () => void;
|
||||
onOpenSkills: () => void;
|
||||
onOpenAutomations: () => void;
|
||||
onOpenSearch: () => void;
|
||||
activeUtility?: "apps" | "skills" | null;
|
||||
activeUtility?: "apps" | "skills" | "automations" | null;
|
||||
onToggleArchived: () => void;
|
||||
onCollapse: () => void;
|
||||
onExpand?: () => void;
|
||||
@@ -49,7 +51,7 @@ interface SidebarProps {
|
||||
projectNameOverrides?: Record<string, string>;
|
||||
collapsedGroups?: Record<string, boolean>;
|
||||
runningChatIds?: string[];
|
||||
completedChatIds?: string[];
|
||||
updatedChatIds?: string[];
|
||||
viewState?: SidebarViewState;
|
||||
showArchived?: boolean;
|
||||
archivedCount?: number;
|
||||
@@ -166,6 +168,13 @@ export function Sidebar(props: SidebarProps) {
|
||||
active={props.activeUtility === "skills"}
|
||||
icon={<Brain className="h-4 w-4" />}
|
||||
/>
|
||||
<SidebarActionButton
|
||||
collapsed={collapsed}
|
||||
label={t("sidebar.automations", { defaultValue: "Automations" })}
|
||||
onClick={props.onOpenAutomations}
|
||||
active={props.activeUtility === "automations"}
|
||||
icon={<CalendarClock className="h-4 w-4" />}
|
||||
/>
|
||||
{props.archivedCount ? (
|
||||
<SidebarActionButton
|
||||
collapsed={collapsed}
|
||||
@@ -201,7 +210,7 @@ export function Sidebar(props: SidebarProps) {
|
||||
projectNameOverrides={props.projectNameOverrides}
|
||||
collapsedGroups={props.collapsedGroups}
|
||||
runningChatIds={props.runningChatIds}
|
||||
completedChatIds={props.completedChatIds}
|
||||
updatedChatIds={props.updatedChatIds}
|
||||
density={props.viewState?.density}
|
||||
showPreviews={props.viewState?.show_previews}
|
||||
showTimestamps={props.viewState?.show_timestamps}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -55,6 +55,7 @@
|
||||
"ariaLabel": "Change language"
|
||||
},
|
||||
"apps": "Apps",
|
||||
"automations": "Automations",
|
||||
"skills": {
|
||||
"title": "Skills"
|
||||
}
|
||||
@@ -80,6 +81,7 @@
|
||||
"runtime": "System",
|
||||
"advanced": "Security",
|
||||
"apps": "Apps",
|
||||
"automations": "Automations",
|
||||
"skills": "Skills"
|
||||
},
|
||||
"sections": {
|
||||
@@ -469,6 +471,127 @@
|
||||
"loading": "Loading Apps...",
|
||||
"empty": "No apps match this filter."
|
||||
},
|
||||
"automations": {
|
||||
"filters": {
|
||||
"all": "All",
|
||||
"active": "Active",
|
||||
"paused": "Paused",
|
||||
"failed": "Needs attention",
|
||||
"system": "System"
|
||||
},
|
||||
"sort": {
|
||||
"next": "Next run",
|
||||
"last": "Last run",
|
||||
"updated": "Updated",
|
||||
"name": "Name"
|
||||
},
|
||||
"search": "Search task, message, linked chat, or schedule",
|
||||
"queue": "Queue",
|
||||
"loading": "Loading automations...",
|
||||
"noMatches": "No automations match this view.",
|
||||
"empty": "No automations yet.",
|
||||
"emptyHint": "Create one from where it should run so nanobot keeps the right context.",
|
||||
"oneShot": "One-time",
|
||||
"systemTask": "System-managed automation",
|
||||
"labels": {
|
||||
"schedule": "Schedule",
|
||||
"next": "Next",
|
||||
"origin": "Linked chat",
|
||||
"created": "Created",
|
||||
"updated": "Updated"
|
||||
},
|
||||
"runNow": "Run now",
|
||||
"pause": "Pause",
|
||||
"resume": "Resume",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"protected": "Protected",
|
||||
"editTitle": "Edit automation",
|
||||
"save": "Save",
|
||||
"deleteTitle": "Delete automation",
|
||||
"deleteDescription": "This removes {{name}} from the cron store. Past chat messages stay in the session.",
|
||||
"cancel": "Cancel",
|
||||
"status": {
|
||||
"system": "System",
|
||||
"running": "Running now",
|
||||
"paused": "Paused",
|
||||
"failed": "Failed",
|
||||
"completed": "Completed",
|
||||
"noSchedule": "No schedule",
|
||||
"active": "Active"
|
||||
},
|
||||
"origin": {
|
||||
"system": "System",
|
||||
"unknown": "No linked chat"
|
||||
},
|
||||
"channels": {
|
||||
"api": "API",
|
||||
"cli": "CLI",
|
||||
"dingtalk": "DingTalk",
|
||||
"discord": "Discord",
|
||||
"email": "Email",
|
||||
"feishu": "Feishu",
|
||||
"matrix": "Matrix",
|
||||
"msteams": "Microsoft Teams",
|
||||
"qq": "QQ",
|
||||
"slack": "Slack",
|
||||
"telegram": "Telegram",
|
||||
"wechat": "WeChat",
|
||||
"wecom": "WeCom",
|
||||
"weixin": "WeChat",
|
||||
"whatsapp": "WhatsApp"
|
||||
},
|
||||
"schedule": {
|
||||
"at": "At {{time}}",
|
||||
"every": "Every {{duration}}",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"withTz": "{{summary}} · {{tz}}",
|
||||
"dailyAt": "Daily at {{time}}",
|
||||
"weekdaysAt": "Weekdays at {{time}}",
|
||||
"hourlyAt": "Hourly at :{{minute}}",
|
||||
"hourlyWindow": "Hourly {{start}}-{{end}} at :{{minute}}",
|
||||
"custom": "Custom schedule"
|
||||
},
|
||||
"next": {
|
||||
"paused": "Paused",
|
||||
"pending": "Running now",
|
||||
"none": "No next run"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "Show full message",
|
||||
"showLess": "Show less"
|
||||
},
|
||||
"fields": {
|
||||
"name": "Name",
|
||||
"message": "Message",
|
||||
"scheduleType": "Schedule type",
|
||||
"every": "Every",
|
||||
"unit": "Unit",
|
||||
"cronExpression": "Cron expression",
|
||||
"timezone": "Timezone",
|
||||
"runAt": "Run at"
|
||||
},
|
||||
"scheduleTypes": {
|
||||
"every": "Interval",
|
||||
"cron": "Cron",
|
||||
"at": "Once"
|
||||
},
|
||||
"everyUnits": {
|
||||
"second": "Seconds",
|
||||
"minute": "Minutes",
|
||||
"hour": "Hours",
|
||||
"day": "Days"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "Name is required.",
|
||||
"messageRequired": "Message is required.",
|
||||
"intervalRequired": "Interval must be a positive number.",
|
||||
"cronRequired": "Cron expression is required.",
|
||||
"timeRequired": "Run time is required.",
|
||||
"futureRequired": "Run time must be in the future."
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
"authentication": "OAuth authentication",
|
||||
"signIn": "Sign in",
|
||||
@@ -527,7 +650,8 @@
|
||||
"newInProject": "Start a new chat in {{project}}",
|
||||
"activity": {
|
||||
"running": "Agent running",
|
||||
"complete": "Agent finished"
|
||||
"complete": "Agent finished",
|
||||
"updated": "New activity"
|
||||
},
|
||||
"pin": "Pin",
|
||||
"unpin": "Unpin",
|
||||
@@ -562,7 +686,7 @@
|
||||
"confirm": "Delete",
|
||||
"automationsDescription": "This chat has scheduled automations. Deleting it will also delete them.",
|
||||
"moreAutomations": "+ {{count}} more",
|
||||
"confirmWithAutomations": "Delete chat and automations",
|
||||
"confirmWithAutomations": "Delete",
|
||||
"schedule": {
|
||||
"at": "{{time}}",
|
||||
"every": "Every {{duration}}",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"ariaLabel": "Cambiar idioma"
|
||||
},
|
||||
"apps": "Apps",
|
||||
"automations": "Automatizaciones",
|
||||
"skills": {
|
||||
"title": "Habilidades"
|
||||
}
|
||||
@@ -80,6 +81,7 @@
|
||||
"cliApps": "Apps CLI",
|
||||
"mcp": "MCP",
|
||||
"apps": "Aplicaciones",
|
||||
"automations": "Automatizaciones",
|
||||
"skills": "Habilidades"
|
||||
},
|
||||
"sections": {
|
||||
@@ -469,6 +471,127 @@
|
||||
"loading": "Cargando apps...",
|
||||
"empty": "Ninguna app coincide con este filtro."
|
||||
},
|
||||
"automations": {
|
||||
"filters": {
|
||||
"all": "Todas",
|
||||
"active": "Activas",
|
||||
"paused": "Pausadas",
|
||||
"failed": "Requieren atención",
|
||||
"system": "Sistema"
|
||||
},
|
||||
"sort": {
|
||||
"next": "Próxima ejecución",
|
||||
"last": "Última ejecución",
|
||||
"updated": "Actualizada",
|
||||
"name": "Nombre"
|
||||
},
|
||||
"search": "Buscar tarea, mensaje, chat vinculado u horario",
|
||||
"queue": "Cola",
|
||||
"loading": "Cargando automatizaciones...",
|
||||
"noMatches": "No hay automatizaciones que coincidan con esta vista.",
|
||||
"empty": "Aún no hay automatizaciones.",
|
||||
"emptyHint": "Créala desde donde debe ejecutarse para que nanobot conserve el contexto correcto.",
|
||||
"oneShot": "Una vez",
|
||||
"systemTask": "Automatización administrada por el sistema",
|
||||
"labels": {
|
||||
"schedule": "Programación",
|
||||
"next": "Siguiente",
|
||||
"origin": "Chat vinculado",
|
||||
"created": "Creada",
|
||||
"updated": "Actualizada"
|
||||
},
|
||||
"runNow": "Ejecutar ahora",
|
||||
"pause": "Pausar",
|
||||
"resume": "Reanudar",
|
||||
"edit": "Editar",
|
||||
"delete": "Eliminar",
|
||||
"protected": "Protegida",
|
||||
"editTitle": "Editar automatización",
|
||||
"save": "Guardar",
|
||||
"deleteTitle": "Eliminar automatización",
|
||||
"deleteDescription": "Esto elimina {{name}} del almacén cron. Los mensajes de chat anteriores permanecen en la sesión.",
|
||||
"cancel": "Cancelar",
|
||||
"status": {
|
||||
"system": "Sistema",
|
||||
"running": "Ejecutándose ahora",
|
||||
"paused": "Pausada",
|
||||
"failed": "Fallida",
|
||||
"completed": "Completada",
|
||||
"noSchedule": "Sin programación",
|
||||
"active": "Activa"
|
||||
},
|
||||
"origin": {
|
||||
"system": "Sistema",
|
||||
"unknown": "Sin chat vinculado"
|
||||
},
|
||||
"channels": {
|
||||
"api": "API",
|
||||
"cli": "CLI",
|
||||
"dingtalk": "DingTalk",
|
||||
"discord": "Discord",
|
||||
"email": "Email",
|
||||
"feishu": "Feishu",
|
||||
"matrix": "Matrix",
|
||||
"msteams": "Microsoft Teams",
|
||||
"qq": "QQ",
|
||||
"slack": "Slack",
|
||||
"telegram": "Telegram",
|
||||
"wechat": "WeChat",
|
||||
"wecom": "WeCom",
|
||||
"weixin": "WeChat",
|
||||
"whatsapp": "WhatsApp"
|
||||
},
|
||||
"schedule": {
|
||||
"at": "A las {{time}}",
|
||||
"every": "Cada {{duration}}",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"withTz": "{{summary}} · {{tz}}",
|
||||
"dailyAt": "Diaria a las {{time}}",
|
||||
"weekdaysAt": "Días laborables a las {{time}}",
|
||||
"hourlyAt": "Cada hora en :{{minute}}",
|
||||
"hourlyWindow": "Cada hora {{start}}-{{end}} en :{{minute}}",
|
||||
"custom": "Programación personalizada"
|
||||
},
|
||||
"next": {
|
||||
"paused": "Pausada",
|
||||
"pending": "Ejecutándose ahora",
|
||||
"none": "Sin próxima ejecución"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "Mostrar mensaje completo",
|
||||
"showLess": "Mostrar menos"
|
||||
},
|
||||
"fields": {
|
||||
"name": "Nombre",
|
||||
"message": "Mensaje",
|
||||
"scheduleType": "Tipo de programación",
|
||||
"every": "Cada",
|
||||
"unit": "Unidad",
|
||||
"cronExpression": "Expresión cron",
|
||||
"timezone": "Zona horaria",
|
||||
"runAt": "Ejecutar a las"
|
||||
},
|
||||
"scheduleTypes": {
|
||||
"every": "Intervalo",
|
||||
"cron": "Cron",
|
||||
"at": "Una vez"
|
||||
},
|
||||
"everyUnits": {
|
||||
"second": "Segundos",
|
||||
"minute": "Minutos",
|
||||
"hour": "Horas",
|
||||
"day": "Días"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "El nombre es obligatorio.",
|
||||
"messageRequired": "El mensaje es obligatorio.",
|
||||
"intervalRequired": "El intervalo debe ser un número positivo.",
|
||||
"cronRequired": "La expresión cron es obligatoria.",
|
||||
"timeRequired": "La hora de ejecución es obligatoria.",
|
||||
"futureRequired": "La hora de ejecución debe estar en el futuro."
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
"authentication": "Autenticación OAuth",
|
||||
"signIn": "Iniciar sesión",
|
||||
@@ -527,7 +650,8 @@
|
||||
"newInProject": "Iniciar un chat nuevo en {{project}}",
|
||||
"activity": {
|
||||
"running": "Agent running",
|
||||
"complete": "Agent finished"
|
||||
"complete": "Agent finished",
|
||||
"updated": "New activity"
|
||||
},
|
||||
"pin": "Pin",
|
||||
"unpin": "Unpin",
|
||||
@@ -562,7 +686,7 @@
|
||||
"confirm": "Eliminar",
|
||||
"automationsDescription": "Este chat tiene automatizaciones programadas. Al eliminarlo también se eliminarán.",
|
||||
"moreAutomations": "+ {{count}} más",
|
||||
"confirmWithAutomations": "Eliminar chat y automatizaciones",
|
||||
"confirmWithAutomations": "Eliminar",
|
||||
"schedule": {
|
||||
"at": "{{time}}",
|
||||
"every": "Cada {{duration}}",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"ariaLabel": "Changer de langue"
|
||||
},
|
||||
"apps": "Apps",
|
||||
"automations": "Automatisations",
|
||||
"skills": {
|
||||
"title": "Compétences"
|
||||
}
|
||||
@@ -80,6 +81,7 @@
|
||||
"cliApps": "Apps CLI",
|
||||
"mcp": "MCP",
|
||||
"apps": "Applications",
|
||||
"automations": "Automatisations",
|
||||
"skills": "Compétences"
|
||||
},
|
||||
"sections": {
|
||||
@@ -469,6 +471,127 @@
|
||||
"loading": "Chargement des apps...",
|
||||
"empty": "Aucune app ne correspond."
|
||||
},
|
||||
"automations": {
|
||||
"filters": {
|
||||
"all": "Toutes",
|
||||
"active": "Actives",
|
||||
"paused": "En pause",
|
||||
"failed": "À traiter",
|
||||
"system": "Système"
|
||||
},
|
||||
"sort": {
|
||||
"next": "Prochaine exécution",
|
||||
"last": "Dernière exécution",
|
||||
"updated": "Mise à jour",
|
||||
"name": "Nom"
|
||||
},
|
||||
"search": "Rechercher tâche, message, discussion liée ou planning",
|
||||
"queue": "File",
|
||||
"loading": "Chargement des automatisations...",
|
||||
"noMatches": "Aucune automatisation ne correspond à cette vue.",
|
||||
"empty": "Aucune automatisation pour le moment.",
|
||||
"emptyHint": "Créez-la depuis son point d'exécution pour que nanobot conserve le bon contexte.",
|
||||
"oneShot": "Ponctuelle",
|
||||
"systemTask": "Automatisation gérée par le système",
|
||||
"labels": {
|
||||
"schedule": "Planning",
|
||||
"next": "Prochaine",
|
||||
"origin": "Discussion liée",
|
||||
"created": "Créée",
|
||||
"updated": "Modifiée"
|
||||
},
|
||||
"runNow": "Exécuter maintenant",
|
||||
"pause": "Mettre en pause",
|
||||
"resume": "Reprendre",
|
||||
"edit": "Modifier",
|
||||
"delete": "Supprimer",
|
||||
"protected": "Protégée",
|
||||
"editTitle": "Modifier l’automatisation",
|
||||
"save": "Enregistrer",
|
||||
"deleteTitle": "Supprimer l’automatisation",
|
||||
"deleteDescription": "Cela supprime {{name}} du stockage cron. Les anciens messages de chat restent dans la session.",
|
||||
"cancel": "Annuler",
|
||||
"status": {
|
||||
"system": "Système",
|
||||
"running": "En cours d’exécution",
|
||||
"paused": "En pause",
|
||||
"failed": "Échouée",
|
||||
"completed": "Terminée",
|
||||
"noSchedule": "Aucun planning",
|
||||
"active": "En cours"
|
||||
},
|
||||
"origin": {
|
||||
"system": "Système",
|
||||
"unknown": "Aucune discussion liée"
|
||||
},
|
||||
"channels": {
|
||||
"api": "API",
|
||||
"cli": "CLI",
|
||||
"dingtalk": "DingTalk",
|
||||
"discord": "Discord",
|
||||
"email": "Email",
|
||||
"feishu": "Feishu",
|
||||
"matrix": "Matrix",
|
||||
"msteams": "Microsoft Teams",
|
||||
"qq": "QQ",
|
||||
"slack": "Slack",
|
||||
"telegram": "Telegram",
|
||||
"wechat": "WeChat",
|
||||
"wecom": "WeCom",
|
||||
"weixin": "WeChat",
|
||||
"whatsapp": "WhatsApp"
|
||||
},
|
||||
"schedule": {
|
||||
"at": "À {{time}}",
|
||||
"every": "Toutes les {{duration}}",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"withTz": "{{summary}} · {{tz}}",
|
||||
"dailyAt": "Chaque jour à {{time}}",
|
||||
"weekdaysAt": "Jours ouvrés à {{time}}",
|
||||
"hourlyAt": "Toutes les heures à :{{minute}}",
|
||||
"hourlyWindow": "Toutes les heures {{start}}-{{end}} à :{{minute}}",
|
||||
"custom": "Planning personnalisé"
|
||||
},
|
||||
"next": {
|
||||
"paused": "En pause",
|
||||
"pending": "En cours d’exécution",
|
||||
"none": "Aucune prochaine exécution"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "Afficher le message complet",
|
||||
"showLess": "Afficher moins"
|
||||
},
|
||||
"fields": {
|
||||
"name": "Nom",
|
||||
"message": "Message",
|
||||
"scheduleType": "Type de planning",
|
||||
"every": "Toutes les",
|
||||
"unit": "Unité",
|
||||
"cronExpression": "Expression cron",
|
||||
"timezone": "Fuseau horaire",
|
||||
"runAt": "Exécuter à"
|
||||
},
|
||||
"scheduleTypes": {
|
||||
"every": "Intervalle",
|
||||
"cron": "Cron",
|
||||
"at": "Une fois"
|
||||
},
|
||||
"everyUnits": {
|
||||
"second": "Secondes",
|
||||
"minute": "Minutes",
|
||||
"hour": "Heures",
|
||||
"day": "Jours"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "Le nom est obligatoire.",
|
||||
"messageRequired": "Le message est obligatoire.",
|
||||
"intervalRequired": "L’intervalle doit être un nombre positif.",
|
||||
"cronRequired": "L’expression cron est obligatoire.",
|
||||
"timeRequired": "L’heure d’exécution est obligatoire.",
|
||||
"futureRequired": "L’heure d’exécution doit être dans le futur."
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
"authentication": "Authentification OAuth",
|
||||
"signIn": "Se connecter",
|
||||
@@ -527,7 +650,8 @@
|
||||
"newInProject": "Démarrer une nouvelle discussion dans {{project}}",
|
||||
"activity": {
|
||||
"running": "Agent running",
|
||||
"complete": "Agent finished"
|
||||
"complete": "Agent finished",
|
||||
"updated": "New activity"
|
||||
},
|
||||
"pin": "Pin",
|
||||
"unpin": "Unpin",
|
||||
@@ -562,7 +686,7 @@
|
||||
"confirm": "Supprimer",
|
||||
"automationsDescription": "Cette discussion contient des automatisations planifiées. La supprimer les supprimera aussi.",
|
||||
"moreAutomations": "+ {{count}} autres",
|
||||
"confirmWithAutomations": "Supprimer la discussion et les automatisations",
|
||||
"confirmWithAutomations": "Supprimer",
|
||||
"schedule": {
|
||||
"at": "{{time}}",
|
||||
"every": "Tous les {{duration}}",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"ariaLabel": "Ganti bahasa"
|
||||
},
|
||||
"apps": "Aplikasi",
|
||||
"automations": "Otomasi",
|
||||
"skills": {
|
||||
"title": "Skill"
|
||||
}
|
||||
@@ -80,6 +81,7 @@
|
||||
"cliApps": "Aplikasi CLI",
|
||||
"mcp": "MCP",
|
||||
"apps": "Aplikasi",
|
||||
"automations": "Otomasi",
|
||||
"skills": "Skill"
|
||||
},
|
||||
"sections": {
|
||||
@@ -469,6 +471,127 @@
|
||||
"loading": "Memuat aplikasi...",
|
||||
"empty": "Tidak ada aplikasi yang cocok."
|
||||
},
|
||||
"automations": {
|
||||
"filters": {
|
||||
"all": "Semua",
|
||||
"active": "Aktif",
|
||||
"paused": "Dijeda",
|
||||
"failed": "Perlu ditangani",
|
||||
"system": "Sistem"
|
||||
},
|
||||
"sort": {
|
||||
"next": "Jalankan berikutnya",
|
||||
"last": "Jalankan terakhir",
|
||||
"updated": "Diperbarui",
|
||||
"name": "Nama"
|
||||
},
|
||||
"search": "Cari tugas, pesan, chat terkait, atau jadwal",
|
||||
"queue": "Antrean",
|
||||
"loading": "Memuat otomasi...",
|
||||
"noMatches": "Tidak ada otomasi yang cocok dengan tampilan ini.",
|
||||
"empty": "Belum ada otomasi.",
|
||||
"emptyHint": "Buat dari tempat tugas ini berjalan agar nanobot menyimpan konteks yang tepat.",
|
||||
"oneShot": "Satu kali",
|
||||
"systemTask": "Automasi yang dikelola sistem",
|
||||
"labels": {
|
||||
"schedule": "Jadwal",
|
||||
"next": "Berikutnya",
|
||||
"origin": "Chat tertaut",
|
||||
"created": "Dibuat",
|
||||
"updated": "Diperbarui"
|
||||
},
|
||||
"runNow": "Jalankan sekarang",
|
||||
"pause": "Jeda",
|
||||
"resume": "Lanjutkan",
|
||||
"edit": "Edit",
|
||||
"delete": "Hapus",
|
||||
"protected": "Terlindungi",
|
||||
"editTitle": "Edit otomasi",
|
||||
"save": "Simpan",
|
||||
"deleteTitle": "Hapus otomasi",
|
||||
"deleteDescription": "Ini menghapus {{name}} dari penyimpanan cron. Pesan chat sebelumnya tetap ada di sesi.",
|
||||
"cancel": "Batal",
|
||||
"status": {
|
||||
"system": "Sistem",
|
||||
"running": "Sedang berjalan",
|
||||
"paused": "Dijeda",
|
||||
"failed": "Gagal",
|
||||
"completed": "Selesai",
|
||||
"noSchedule": "Tanpa jadwal",
|
||||
"active": "Aktif"
|
||||
},
|
||||
"origin": {
|
||||
"system": "Sistem",
|
||||
"unknown": "Tidak ada chat tertaut"
|
||||
},
|
||||
"channels": {
|
||||
"api": "API",
|
||||
"cli": "CLI",
|
||||
"dingtalk": "DingTalk",
|
||||
"discord": "Discord",
|
||||
"email": "Email",
|
||||
"feishu": "Feishu",
|
||||
"matrix": "Matrix",
|
||||
"msteams": "Microsoft Teams",
|
||||
"qq": "QQ",
|
||||
"slack": "Slack",
|
||||
"telegram": "Telegram",
|
||||
"wechat": "WeChat",
|
||||
"wecom": "WeCom",
|
||||
"weixin": "WeChat",
|
||||
"whatsapp": "WhatsApp"
|
||||
},
|
||||
"schedule": {
|
||||
"at": "Pada {{time}}",
|
||||
"every": "Setiap {{duration}}",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"withTz": "{{summary}} · {{tz}}",
|
||||
"dailyAt": "Setiap hari pukul {{time}}",
|
||||
"weekdaysAt": "Hari kerja pukul {{time}}",
|
||||
"hourlyAt": "Setiap jam pada :{{minute}}",
|
||||
"hourlyWindow": "Setiap jam {{start}}-{{end}} pada :{{minute}}",
|
||||
"custom": "Jadwal khusus"
|
||||
},
|
||||
"next": {
|
||||
"paused": "Dijeda",
|
||||
"pending": "Sedang berjalan",
|
||||
"none": "Tidak ada jadwal berikutnya"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "Tampilkan pesan lengkap",
|
||||
"showLess": "Tampilkan lebih sedikit"
|
||||
},
|
||||
"fields": {
|
||||
"name": "Nama",
|
||||
"message": "Pesan",
|
||||
"scheduleType": "Jenis jadwal",
|
||||
"every": "Setiap",
|
||||
"unit": "Unit",
|
||||
"cronExpression": "Ekspresi cron",
|
||||
"timezone": "Zona waktu",
|
||||
"runAt": "Jalankan pada"
|
||||
},
|
||||
"scheduleTypes": {
|
||||
"every": "Interval",
|
||||
"cron": "Cron",
|
||||
"at": "Sekali"
|
||||
},
|
||||
"everyUnits": {
|
||||
"second": "Detik",
|
||||
"minute": "Menit",
|
||||
"hour": "Jam",
|
||||
"day": "Hari"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "Nama wajib diisi.",
|
||||
"messageRequired": "Pesan wajib diisi.",
|
||||
"intervalRequired": "Interval harus berupa angka positif.",
|
||||
"cronRequired": "Ekspresi cron wajib diisi.",
|
||||
"timeRequired": "Waktu eksekusi wajib diisi.",
|
||||
"futureRequired": "Waktu eksekusi harus berada di masa depan."
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
"authentication": "Autentikasi OAuth",
|
||||
"signIn": "Masuk",
|
||||
@@ -527,7 +650,8 @@
|
||||
"newInProject": "Mulai obrolan baru di {{project}}",
|
||||
"activity": {
|
||||
"running": "Agent running",
|
||||
"complete": "Agent finished"
|
||||
"complete": "Agent finished",
|
||||
"updated": "New activity"
|
||||
},
|
||||
"pin": "Pin",
|
||||
"unpin": "Unpin",
|
||||
@@ -562,7 +686,7 @@
|
||||
"confirm": "Hapus",
|
||||
"automationsDescription": "Obrolan ini memiliki automasi terjadwal. Menghapusnya juga akan menghapus automasi tersebut.",
|
||||
"moreAutomations": "+ {{count}} lagi",
|
||||
"confirmWithAutomations": "Hapus obrolan dan automasi",
|
||||
"confirmWithAutomations": "Hapus",
|
||||
"schedule": {
|
||||
"at": "{{time}}",
|
||||
"every": "Setiap {{duration}}",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"ariaLabel": "言語を変更"
|
||||
},
|
||||
"apps": "アプリ",
|
||||
"automations": "自動タスク",
|
||||
"skills": {
|
||||
"title": "スキル"
|
||||
}
|
||||
@@ -80,6 +81,7 @@
|
||||
"cliApps": "CLI アプリ",
|
||||
"mcp": "MCP",
|
||||
"apps": "アプリ",
|
||||
"automations": "自動タスク",
|
||||
"skills": "スキル"
|
||||
},
|
||||
"sections": {
|
||||
@@ -469,6 +471,127 @@
|
||||
"loading": "アプリを読み込み中...",
|
||||
"empty": "一致するアプリはありません。"
|
||||
},
|
||||
"automations": {
|
||||
"filters": {
|
||||
"all": "すべて",
|
||||
"active": "実行中",
|
||||
"paused": "一時停止",
|
||||
"failed": "要対応",
|
||||
"system": "システム"
|
||||
},
|
||||
"sort": {
|
||||
"next": "次回実行",
|
||||
"last": "前回実行",
|
||||
"updated": "更新日時",
|
||||
"name": "名前"
|
||||
},
|
||||
"search": "タスク、メッセージ、関連チャット、予定を検索",
|
||||
"queue": "キュー",
|
||||
"loading": "自動タスクを読み込み中...",
|
||||
"noMatches": "この表示に一致する自動タスクはありません。",
|
||||
"empty": "自動タスクはまだありません。",
|
||||
"emptyHint": "実行元から作成すると、nanobot が正しいコンテキストを保持できます。",
|
||||
"oneShot": "一回限り",
|
||||
"systemTask": "システム管理の自動タスク",
|
||||
"labels": {
|
||||
"schedule": "スケジュール",
|
||||
"next": "次回",
|
||||
"origin": "関連チャット",
|
||||
"created": "作成",
|
||||
"updated": "更新"
|
||||
},
|
||||
"runNow": "今すぐ実行",
|
||||
"pause": "一時停止",
|
||||
"resume": "再開",
|
||||
"edit": "編集",
|
||||
"delete": "削除",
|
||||
"protected": "保護済み",
|
||||
"editTitle": "自動タスクを編集",
|
||||
"save": "保存",
|
||||
"deleteTitle": "自動タスクを削除",
|
||||
"deleteDescription": "{{name}} を cron ストアから削除します。過去のチャットメッセージはセッションに残ります。",
|
||||
"cancel": "キャンセル",
|
||||
"status": {
|
||||
"system": "システム",
|
||||
"running": "実行中",
|
||||
"paused": "一時停止",
|
||||
"failed": "失敗",
|
||||
"completed": "完了",
|
||||
"noSchedule": "スケジュールなし",
|
||||
"active": "実行中"
|
||||
},
|
||||
"origin": {
|
||||
"system": "システム",
|
||||
"unknown": "関連チャットなし"
|
||||
},
|
||||
"channels": {
|
||||
"api": "API",
|
||||
"cli": "CLI",
|
||||
"dingtalk": "DingTalk",
|
||||
"discord": "Discord",
|
||||
"email": "Email",
|
||||
"feishu": "Feishu",
|
||||
"matrix": "Matrix",
|
||||
"msteams": "Microsoft Teams",
|
||||
"qq": "QQ",
|
||||
"slack": "Slack",
|
||||
"telegram": "Telegram",
|
||||
"wechat": "WeChat",
|
||||
"wecom": "WeCom",
|
||||
"weixin": "WeChat",
|
||||
"whatsapp": "WhatsApp"
|
||||
},
|
||||
"schedule": {
|
||||
"at": "{{time}}",
|
||||
"every": "{{duration}} ごと",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"withTz": "{{summary}} · {{tz}}",
|
||||
"dailyAt": "毎日 {{time}}",
|
||||
"weekdaysAt": "平日 {{time}}",
|
||||
"hourlyAt": "毎時 :{{minute}}",
|
||||
"hourlyWindow": "{{start}}-{{end}} の毎時 :{{minute}}",
|
||||
"custom": "カスタムスケジュール"
|
||||
},
|
||||
"next": {
|
||||
"paused": "一時停止",
|
||||
"pending": "実行中",
|
||||
"none": "次回実行なし"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "メッセージ全文を表示",
|
||||
"showLess": "折りたたむ"
|
||||
},
|
||||
"fields": {
|
||||
"name": "名前",
|
||||
"message": "メッセージ",
|
||||
"scheduleType": "スケジュール種別",
|
||||
"every": "間隔",
|
||||
"unit": "単位",
|
||||
"cronExpression": "Cron 式",
|
||||
"timezone": "タイムゾーン",
|
||||
"runAt": "実行日時"
|
||||
},
|
||||
"scheduleTypes": {
|
||||
"every": "間隔",
|
||||
"cron": "Cron",
|
||||
"at": "一回限り"
|
||||
},
|
||||
"everyUnits": {
|
||||
"second": "秒",
|
||||
"minute": "分",
|
||||
"hour": "時間",
|
||||
"day": "日"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "名前は必須です。",
|
||||
"messageRequired": "メッセージは必須です。",
|
||||
"intervalRequired": "間隔は正の数で指定してください。",
|
||||
"cronRequired": "Cron 式は必須です。",
|
||||
"timeRequired": "実行日時は必須です。",
|
||||
"futureRequired": "実行日時は現在より後にしてください。"
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
"authentication": "OAuth 認証",
|
||||
"signIn": "サインイン",
|
||||
@@ -527,7 +650,8 @@
|
||||
"newInProject": "「{{project}}」で新しいチャットを開始",
|
||||
"activity": {
|
||||
"running": "Agent running",
|
||||
"complete": "Agent finished"
|
||||
"complete": "Agent finished",
|
||||
"updated": "New activity"
|
||||
},
|
||||
"pin": "Pin",
|
||||
"unpin": "Unpin",
|
||||
@@ -562,7 +686,7 @@
|
||||
"confirm": "削除",
|
||||
"automationsDescription": "このチャットにはスケジュール済みの自動タスクがあります。削除するとそれらも削除されます。",
|
||||
"moreAutomations": "他 {{count}} 件",
|
||||
"confirmWithAutomations": "チャットと自動タスクを削除",
|
||||
"confirmWithAutomations": "削除",
|
||||
"schedule": {
|
||||
"at": "{{time}}",
|
||||
"every": "{{duration}} ごと",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"ariaLabel": "언어 변경"
|
||||
},
|
||||
"apps": "앱",
|
||||
"automations": "자동화",
|
||||
"skills": {
|
||||
"title": "스킬"
|
||||
}
|
||||
@@ -80,6 +81,7 @@
|
||||
"cliApps": "CLI 앱",
|
||||
"mcp": "MCP",
|
||||
"apps": "앱",
|
||||
"automations": "자동화",
|
||||
"skills": "스킬"
|
||||
},
|
||||
"sections": {
|
||||
@@ -469,6 +471,127 @@
|
||||
"loading": "앱을 불러오는 중...",
|
||||
"empty": "일치하는 앱이 없습니다."
|
||||
},
|
||||
"automations": {
|
||||
"filters": {
|
||||
"all": "전체",
|
||||
"active": "활성",
|
||||
"paused": "일시 중지",
|
||||
"failed": "확인 필요",
|
||||
"system": "시스템"
|
||||
},
|
||||
"sort": {
|
||||
"next": "다음 실행",
|
||||
"last": "마지막 실행",
|
||||
"updated": "업데이트",
|
||||
"name": "이름"
|
||||
},
|
||||
"search": "작업, 메시지, 연결된 채팅 또는 일정 검색",
|
||||
"queue": "대기열",
|
||||
"loading": "자동화를 불러오는 중...",
|
||||
"noMatches": "이 보기와 일치하는 자동화가 없습니다.",
|
||||
"empty": "아직 자동화가 없습니다.",
|
||||
"emptyHint": "실행될 위치에서 만들면 nanobot이 올바른 컨텍스트를 유지합니다.",
|
||||
"oneShot": "일회성",
|
||||
"systemTask": "시스템 관리 자동화",
|
||||
"labels": {
|
||||
"schedule": "일정",
|
||||
"next": "다음",
|
||||
"origin": "연결된 채팅",
|
||||
"created": "생성",
|
||||
"updated": "업데이트"
|
||||
},
|
||||
"runNow": "지금 실행",
|
||||
"pause": "일시 중지",
|
||||
"resume": "재개",
|
||||
"edit": "편집",
|
||||
"delete": "삭제",
|
||||
"protected": "보호됨",
|
||||
"editTitle": "자동화 편집",
|
||||
"save": "저장",
|
||||
"deleteTitle": "자동화 삭제",
|
||||
"deleteDescription": "{{name}}을 cron 저장소에서 삭제합니다. 이전 채팅 메시지는 세션에 남습니다.",
|
||||
"cancel": "취소",
|
||||
"status": {
|
||||
"system": "시스템",
|
||||
"running": "실행 중",
|
||||
"paused": "일시 중지",
|
||||
"failed": "실패",
|
||||
"completed": "완료",
|
||||
"noSchedule": "일정 없음",
|
||||
"active": "활성"
|
||||
},
|
||||
"origin": {
|
||||
"system": "시스템",
|
||||
"unknown": "연결된 채팅 없음"
|
||||
},
|
||||
"channels": {
|
||||
"api": "API",
|
||||
"cli": "CLI",
|
||||
"dingtalk": "DingTalk",
|
||||
"discord": "Discord",
|
||||
"email": "Email",
|
||||
"feishu": "Feishu",
|
||||
"matrix": "Matrix",
|
||||
"msteams": "Microsoft Teams",
|
||||
"qq": "QQ",
|
||||
"slack": "Slack",
|
||||
"telegram": "Telegram",
|
||||
"wechat": "WeChat",
|
||||
"wecom": "WeCom",
|
||||
"weixin": "WeChat",
|
||||
"whatsapp": "WhatsApp"
|
||||
},
|
||||
"schedule": {
|
||||
"at": "{{time}}",
|
||||
"every": "{{duration}}마다",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"withTz": "{{summary}} · {{tz}}",
|
||||
"dailyAt": "매일 {{time}}",
|
||||
"weekdaysAt": "평일 {{time}}",
|
||||
"hourlyAt": "매시간 :{{minute}}",
|
||||
"hourlyWindow": "{{start}}-{{end}} 사이 매시간 :{{minute}}",
|
||||
"custom": "사용자 지정 일정"
|
||||
},
|
||||
"next": {
|
||||
"paused": "일시 중지",
|
||||
"pending": "실행 중",
|
||||
"none": "다음 실행 없음"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "전체 메시지 보기",
|
||||
"showLess": "접기"
|
||||
},
|
||||
"fields": {
|
||||
"name": "이름",
|
||||
"message": "메시지",
|
||||
"scheduleType": "일정 유형",
|
||||
"every": "간격",
|
||||
"unit": "단위",
|
||||
"cronExpression": "Cron 식",
|
||||
"timezone": "시간대",
|
||||
"runAt": "실행 시간"
|
||||
},
|
||||
"scheduleTypes": {
|
||||
"every": "간격",
|
||||
"cron": "Cron",
|
||||
"at": "일회성"
|
||||
},
|
||||
"everyUnits": {
|
||||
"second": "초",
|
||||
"minute": "분",
|
||||
"hour": "시간",
|
||||
"day": "일"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "이름은 필수입니다.",
|
||||
"messageRequired": "메시지는 필수입니다.",
|
||||
"intervalRequired": "간격은 양수여야 합니다.",
|
||||
"cronRequired": "Cron 식은 필수입니다.",
|
||||
"timeRequired": "실행 시간은 필수입니다.",
|
||||
"futureRequired": "실행 시간은 현재보다 이후여야 합니다."
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
"authentication": "OAuth 인증",
|
||||
"signIn": "로그인",
|
||||
@@ -527,7 +650,8 @@
|
||||
"newInProject": "{{project}}에서 새 채팅 시작",
|
||||
"activity": {
|
||||
"running": "Agent running",
|
||||
"complete": "Agent finished"
|
||||
"complete": "Agent finished",
|
||||
"updated": "New activity"
|
||||
},
|
||||
"pin": "Pin",
|
||||
"unpin": "Unpin",
|
||||
@@ -562,7 +686,7 @@
|
||||
"confirm": "삭제",
|
||||
"automationsDescription": "이 채팅에는 예약된 자동화가 있습니다. 채팅을 삭제하면 자동화도 함께 삭제됩니다.",
|
||||
"moreAutomations": "+ {{count}}개 더",
|
||||
"confirmWithAutomations": "채팅과 자동화 삭제",
|
||||
"confirmWithAutomations": "삭제",
|
||||
"schedule": {
|
||||
"at": "{{time}}",
|
||||
"every": "{{duration}}마다",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"ariaLabel": "Đổi ngôn ngữ"
|
||||
},
|
||||
"apps": "Ứng dụng",
|
||||
"automations": "Tự động hóa",
|
||||
"skills": {
|
||||
"title": "Kỹ năng"
|
||||
}
|
||||
@@ -80,6 +81,7 @@
|
||||
"cliApps": "Ứng dụng CLI",
|
||||
"mcp": "MCP",
|
||||
"apps": "Ứng dụng",
|
||||
"automations": "Tự động hóa",
|
||||
"skills": "Kỹ năng"
|
||||
},
|
||||
"sections": {
|
||||
@@ -469,6 +471,127 @@
|
||||
"loading": "Đang tải ứng dụng...",
|
||||
"empty": "Không có ứng dụng phù hợp."
|
||||
},
|
||||
"automations": {
|
||||
"filters": {
|
||||
"all": "Tất cả",
|
||||
"active": "Đang chạy",
|
||||
"paused": "Đã tạm dừng",
|
||||
"failed": "Cần xử lý",
|
||||
"system": "Hệ thống"
|
||||
},
|
||||
"sort": {
|
||||
"next": "Lần chạy tiếp theo",
|
||||
"last": "Lần chạy trước",
|
||||
"updated": "Đã cập nhật",
|
||||
"name": "Tên"
|
||||
},
|
||||
"search": "Tìm tác vụ, tin nhắn, cuộc trò chuyện liên kết hoặc lịch",
|
||||
"queue": "Hàng đợi",
|
||||
"loading": "Đang tải tự động hóa...",
|
||||
"noMatches": "Không có tự động hóa phù hợp với chế độ xem này.",
|
||||
"empty": "Chưa có tự động hóa.",
|
||||
"emptyHint": "Tạo từ nơi tác vụ sẽ chạy để nanobot giữ đúng ngữ cảnh.",
|
||||
"oneShot": "Một lần",
|
||||
"systemTask": "Tự động hóa do hệ thống quản lý",
|
||||
"labels": {
|
||||
"schedule": "Lịch",
|
||||
"next": "Tiếp theo",
|
||||
"origin": "Cuộc trò chuyện liên kết",
|
||||
"created": "Đã tạo",
|
||||
"updated": "Đã cập nhật"
|
||||
},
|
||||
"runNow": "Chạy ngay",
|
||||
"pause": "Tạm dừng",
|
||||
"resume": "Tiếp tục",
|
||||
"edit": "Sửa",
|
||||
"delete": "Xóa",
|
||||
"protected": "Được bảo vệ",
|
||||
"editTitle": "Sửa tự động hóa",
|
||||
"save": "Lưu",
|
||||
"deleteTitle": "Xóa tự động hóa",
|
||||
"deleteDescription": "Thao tác này xóa {{name}} khỏi kho cron. Tin nhắn chat trước đó vẫn ở trong phiên.",
|
||||
"cancel": "Hủy",
|
||||
"status": {
|
||||
"system": "Hệ thống",
|
||||
"running": "Đang chạy",
|
||||
"paused": "Đã tạm dừng",
|
||||
"failed": "Thất bại",
|
||||
"completed": "Hoàn tất",
|
||||
"noSchedule": "Không có lịch",
|
||||
"active": "Đang chạy"
|
||||
},
|
||||
"origin": {
|
||||
"system": "Hệ thống",
|
||||
"unknown": "Chưa liên kết cuộc trò chuyện"
|
||||
},
|
||||
"channels": {
|
||||
"api": "API",
|
||||
"cli": "CLI",
|
||||
"dingtalk": "DingTalk",
|
||||
"discord": "Discord",
|
||||
"email": "Email",
|
||||
"feishu": "Feishu",
|
||||
"matrix": "Matrix",
|
||||
"msteams": "Microsoft Teams",
|
||||
"qq": "QQ",
|
||||
"slack": "Slack",
|
||||
"telegram": "Telegram",
|
||||
"wechat": "WeChat",
|
||||
"wecom": "WeCom",
|
||||
"weixin": "WeChat",
|
||||
"whatsapp": "WhatsApp"
|
||||
},
|
||||
"schedule": {
|
||||
"at": "Vào {{time}}",
|
||||
"every": "Mỗi {{duration}}",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"withTz": "{{summary}} · {{tz}}",
|
||||
"dailyAt": "Hằng ngày lúc {{time}}",
|
||||
"weekdaysAt": "Ngày làm việc lúc {{time}}",
|
||||
"hourlyAt": "Mỗi giờ tại :{{minute}}",
|
||||
"hourlyWindow": "Mỗi giờ {{start}}-{{end}} tại :{{minute}}",
|
||||
"custom": "Lịch tùy chỉnh"
|
||||
},
|
||||
"next": {
|
||||
"paused": "Đã tạm dừng",
|
||||
"pending": "Đang chạy",
|
||||
"none": "Không có lần chạy tiếp theo"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "Hiển thị toàn bộ tin nhắn",
|
||||
"showLess": "Thu gọn"
|
||||
},
|
||||
"fields": {
|
||||
"name": "Tên",
|
||||
"message": "Tin nhắn",
|
||||
"scheduleType": "Loại lịch",
|
||||
"every": "Mỗi",
|
||||
"unit": "Đơn vị",
|
||||
"cronExpression": "Biểu thức cron",
|
||||
"timezone": "Múi giờ",
|
||||
"runAt": "Chạy lúc"
|
||||
},
|
||||
"scheduleTypes": {
|
||||
"every": "Khoảng lặp",
|
||||
"cron": "Cron",
|
||||
"at": "Một lần"
|
||||
},
|
||||
"everyUnits": {
|
||||
"second": "Giây",
|
||||
"minute": "Phút",
|
||||
"hour": "Giờ",
|
||||
"day": "Ngày"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "Tên là bắt buộc.",
|
||||
"messageRequired": "Tin nhắn là bắt buộc.",
|
||||
"intervalRequired": "Khoảng lặp phải là số dương.",
|
||||
"cronRequired": "Biểu thức cron là bắt buộc.",
|
||||
"timeRequired": "Thời gian chạy là bắt buộc.",
|
||||
"futureRequired": "Thời gian chạy phải ở tương lai."
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
"authentication": "Xác thực OAuth",
|
||||
"signIn": "Đăng nhập",
|
||||
@@ -527,7 +650,8 @@
|
||||
"newInProject": "Bắt đầu cuộc trò chuyện mới trong {{project}}",
|
||||
"activity": {
|
||||
"running": "Agent running",
|
||||
"complete": "Agent finished"
|
||||
"complete": "Agent finished",
|
||||
"updated": "New activity"
|
||||
},
|
||||
"pin": "Pin",
|
||||
"unpin": "Unpin",
|
||||
@@ -562,7 +686,7 @@
|
||||
"confirm": "Xóa",
|
||||
"automationsDescription": "Cuộc trò chuyện này có các tự động hóa đã lên lịch. Xóa cuộc trò chuyện cũng sẽ xóa chúng.",
|
||||
"moreAutomations": "+ {{count}} mục nữa",
|
||||
"confirmWithAutomations": "Xóa trò chuyện và tự động hóa",
|
||||
"confirmWithAutomations": "Xóa",
|
||||
"schedule": {
|
||||
"at": "{{time}}",
|
||||
"every": "Mỗi {{duration}}",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"ariaLabel": "切换语言"
|
||||
},
|
||||
"apps": "应用",
|
||||
"automations": "自动任务",
|
||||
"skills": {
|
||||
"title": "技能"
|
||||
}
|
||||
@@ -80,6 +81,7 @@
|
||||
"runtime": "系统",
|
||||
"advanced": "安全",
|
||||
"apps": "应用",
|
||||
"automations": "自动任务",
|
||||
"skills": "技能"
|
||||
},
|
||||
"sections": {
|
||||
@@ -469,6 +471,127 @@
|
||||
"loading": "正在加载应用...",
|
||||
"empty": "没有匹配的应用。"
|
||||
},
|
||||
"automations": {
|
||||
"filters": {
|
||||
"all": "全部",
|
||||
"active": "运行中",
|
||||
"paused": "已暂停",
|
||||
"failed": "异常",
|
||||
"system": "系统"
|
||||
},
|
||||
"sort": {
|
||||
"next": "下次运行",
|
||||
"last": "上次运行",
|
||||
"updated": "更新时间",
|
||||
"name": "名称"
|
||||
},
|
||||
"search": "搜索任务、消息、关联会话或计划",
|
||||
"queue": "任务队列",
|
||||
"loading": "正在加载自动任务...",
|
||||
"noMatches": "当前视图没有匹配的自动任务。",
|
||||
"empty": "暂无自动任务。",
|
||||
"emptyHint": "请从它应该运行的来源处创建,这样 nanobot 才能保留正确上下文。",
|
||||
"oneShot": "一次性",
|
||||
"systemTask": "系统管理的自动任务",
|
||||
"labels": {
|
||||
"schedule": "计划",
|
||||
"next": "下次",
|
||||
"origin": "关联会话",
|
||||
"created": "创建于",
|
||||
"updated": "更新于"
|
||||
},
|
||||
"runNow": "立即运行",
|
||||
"pause": "暂停",
|
||||
"resume": "恢复",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"protected": "受保护",
|
||||
"editTitle": "编辑自动任务",
|
||||
"save": "保存",
|
||||
"deleteTitle": "删除自动任务",
|
||||
"deleteDescription": "这会从 cron 存储中删除 {{name}},历史聊天消息会保留在会话中。",
|
||||
"cancel": "取消",
|
||||
"status": {
|
||||
"system": "系统",
|
||||
"running": "正在运行",
|
||||
"paused": "已暂停",
|
||||
"failed": "失败",
|
||||
"completed": "已完成",
|
||||
"noSchedule": "无计划",
|
||||
"active": "运行中"
|
||||
},
|
||||
"origin": {
|
||||
"system": "系统",
|
||||
"unknown": "未关联会话"
|
||||
},
|
||||
"channels": {
|
||||
"api": "API",
|
||||
"cli": "CLI",
|
||||
"dingtalk": "钉钉",
|
||||
"discord": "Discord",
|
||||
"email": "邮件",
|
||||
"feishu": "飞书",
|
||||
"matrix": "Matrix",
|
||||
"msteams": "Microsoft Teams",
|
||||
"qq": "QQ",
|
||||
"slack": "Slack",
|
||||
"telegram": "Telegram",
|
||||
"wechat": "微信",
|
||||
"wecom": "企业微信",
|
||||
"weixin": "微信",
|
||||
"whatsapp": "WhatsApp"
|
||||
},
|
||||
"schedule": {
|
||||
"at": "在 {{time}}",
|
||||
"every": "每 {{duration}}",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"withTz": "{{summary}} · {{tz}}",
|
||||
"dailyAt": "每天 {{time}}",
|
||||
"weekdaysAt": "工作日 {{time}}",
|
||||
"hourlyAt": "每小时第 {{minute}} 分钟",
|
||||
"hourlyWindow": "{{start}}-{{end}} 点每小时第 {{minute}} 分钟",
|
||||
"custom": "自定义计划"
|
||||
},
|
||||
"next": {
|
||||
"paused": "已暂停",
|
||||
"pending": "正在运行",
|
||||
"none": "没有下次运行"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "查看完整消息",
|
||||
"showLess": "收起消息"
|
||||
},
|
||||
"fields": {
|
||||
"name": "名称",
|
||||
"message": "消息",
|
||||
"scheduleType": "计划类型",
|
||||
"every": "每隔",
|
||||
"unit": "单位",
|
||||
"cronExpression": "Cron 表达式",
|
||||
"timezone": "时区",
|
||||
"runAt": "运行时间"
|
||||
},
|
||||
"scheduleTypes": {
|
||||
"every": "间隔",
|
||||
"cron": "Cron",
|
||||
"at": "一次性"
|
||||
},
|
||||
"everyUnits": {
|
||||
"second": "秒",
|
||||
"minute": "分钟",
|
||||
"hour": "小时",
|
||||
"day": "天"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "名称不能为空。",
|
||||
"messageRequired": "消息不能为空。",
|
||||
"intervalRequired": "间隔必须是正整数。",
|
||||
"cronRequired": "Cron 表达式不能为空。",
|
||||
"timeRequired": "运行时间不能为空。",
|
||||
"futureRequired": "运行时间必须晚于当前时间。"
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
"authentication": "OAuth 认证",
|
||||
"signIn": "登录",
|
||||
@@ -527,7 +650,8 @@
|
||||
"newInProject": "在 {{project}} 中开始新对话",
|
||||
"activity": {
|
||||
"running": "Agent 正在运行",
|
||||
"complete": "Agent 已完成"
|
||||
"complete": "Agent 已完成",
|
||||
"updated": "有新内容"
|
||||
},
|
||||
"pin": "置顶",
|
||||
"unpin": "取消置顶",
|
||||
@@ -562,7 +686,7 @@
|
||||
"confirm": "删除",
|
||||
"automationsDescription": "这个对话有关联的自动任务。删除对话也会删除这些自动任务。",
|
||||
"moreAutomations": "另有 {{count}} 个",
|
||||
"confirmWithAutomations": "删除对话和自动任务",
|
||||
"confirmWithAutomations": "删除",
|
||||
"schedule": {
|
||||
"at": "{{time}}",
|
||||
"every": "每 {{duration}}",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"ariaLabel": "切換語言"
|
||||
},
|
||||
"apps": "應用",
|
||||
"automations": "自動任務",
|
||||
"skills": {
|
||||
"title": "技能"
|
||||
}
|
||||
@@ -80,6 +81,7 @@
|
||||
"cliApps": "CLI 應用",
|
||||
"mcp": "MCP",
|
||||
"apps": "應用",
|
||||
"automations": "自動任務",
|
||||
"skills": "技能"
|
||||
},
|
||||
"sections": {
|
||||
@@ -469,6 +471,127 @@
|
||||
"loading": "正在載入應用...",
|
||||
"empty": "沒有符合的應用。"
|
||||
},
|
||||
"automations": {
|
||||
"filters": {
|
||||
"all": "全部",
|
||||
"active": "執行中",
|
||||
"paused": "已暫停",
|
||||
"failed": "異常",
|
||||
"system": "系統"
|
||||
},
|
||||
"sort": {
|
||||
"next": "下次執行",
|
||||
"last": "上次執行",
|
||||
"updated": "更新時間",
|
||||
"name": "名稱"
|
||||
},
|
||||
"search": "搜尋任務、訊息、關聯對話或排程",
|
||||
"queue": "任務佇列",
|
||||
"loading": "正在載入自動任務...",
|
||||
"noMatches": "目前檢視沒有符合的自動任務。",
|
||||
"empty": "尚無自動任務。",
|
||||
"emptyHint": "請從它應該執行的來源處建立,這樣 nanobot 才能保留正確上下文。",
|
||||
"oneShot": "一次性",
|
||||
"systemTask": "系統管理的自動任務",
|
||||
"labels": {
|
||||
"schedule": "排程",
|
||||
"next": "下次",
|
||||
"origin": "關聯會話",
|
||||
"created": "建立於",
|
||||
"updated": "更新於"
|
||||
},
|
||||
"runNow": "立即執行",
|
||||
"pause": "暫停",
|
||||
"resume": "恢復",
|
||||
"edit": "編輯",
|
||||
"delete": "刪除",
|
||||
"protected": "受保護",
|
||||
"editTitle": "編輯自動任務",
|
||||
"save": "儲存",
|
||||
"deleteTitle": "刪除自動任務",
|
||||
"deleteDescription": "這會從 cron 儲存中刪除 {{name}},歷史聊天訊息會保留在會話中。",
|
||||
"cancel": "取消",
|
||||
"status": {
|
||||
"system": "系統",
|
||||
"running": "正在執行",
|
||||
"paused": "已暫停",
|
||||
"failed": "失敗",
|
||||
"completed": "已完成",
|
||||
"noSchedule": "無排程",
|
||||
"active": "執行中"
|
||||
},
|
||||
"origin": {
|
||||
"system": "系統",
|
||||
"unknown": "未關聯會話"
|
||||
},
|
||||
"channels": {
|
||||
"api": "API",
|
||||
"cli": "CLI",
|
||||
"dingtalk": "釘釘",
|
||||
"discord": "Discord",
|
||||
"email": "電子郵件",
|
||||
"feishu": "飛書",
|
||||
"matrix": "Matrix",
|
||||
"msteams": "Microsoft Teams",
|
||||
"qq": "QQ",
|
||||
"slack": "Slack",
|
||||
"telegram": "Telegram",
|
||||
"wechat": "微信",
|
||||
"wecom": "企業微信",
|
||||
"weixin": "微信",
|
||||
"whatsapp": "WhatsApp"
|
||||
},
|
||||
"schedule": {
|
||||
"at": "於 {{time}}",
|
||||
"every": "每 {{duration}}",
|
||||
"cron": "Cron {{expr}}",
|
||||
"cronWithTz": "Cron {{expr}} · {{tz}}",
|
||||
"withTz": "{{summary}} · {{tz}}",
|
||||
"dailyAt": "每天 {{time}}",
|
||||
"weekdaysAt": "工作日 {{time}}",
|
||||
"hourlyAt": "每小時第 {{minute}} 分鐘",
|
||||
"hourlyWindow": "{{start}}-{{end}} 點每小時第 {{minute}} 分鐘",
|
||||
"custom": "自訂排程"
|
||||
},
|
||||
"next": {
|
||||
"paused": "已暫停",
|
||||
"pending": "正在執行",
|
||||
"none": "沒有下次執行"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "查看完整訊息",
|
||||
"showLess": "收起訊息"
|
||||
},
|
||||
"fields": {
|
||||
"name": "名稱",
|
||||
"message": "訊息",
|
||||
"scheduleType": "排程類型",
|
||||
"every": "每隔",
|
||||
"unit": "單位",
|
||||
"cronExpression": "Cron 表達式",
|
||||
"timezone": "時區",
|
||||
"runAt": "執行時間"
|
||||
},
|
||||
"scheduleTypes": {
|
||||
"every": "間隔",
|
||||
"cron": "Cron",
|
||||
"at": "一次性"
|
||||
},
|
||||
"everyUnits": {
|
||||
"second": "秒",
|
||||
"minute": "分鐘",
|
||||
"hour": "小時",
|
||||
"day": "天"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "名稱不能為空。",
|
||||
"messageRequired": "訊息不能為空。",
|
||||
"intervalRequired": "間隔必須是正整數。",
|
||||
"cronRequired": "Cron 表達式不能為空。",
|
||||
"timeRequired": "執行時間不能為空。",
|
||||
"futureRequired": "執行時間必須晚於目前時間。"
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
"authentication": "OAuth 認證",
|
||||
"signIn": "登入",
|
||||
@@ -527,7 +650,8 @@
|
||||
"newInProject": "在 {{project}} 中開始新對話",
|
||||
"activity": {
|
||||
"running": "Agent 正在執行",
|
||||
"complete": "Agent 已完成"
|
||||
"complete": "Agent 已完成",
|
||||
"updated": "有新內容"
|
||||
},
|
||||
"pin": "置頂",
|
||||
"unpin": "取消置頂",
|
||||
@@ -562,7 +686,7 @@
|
||||
"confirm": "刪除",
|
||||
"automationsDescription": "這個對話有關聯的自動任務。刪除對話也會刪除這些自動任務。",
|
||||
"moreAutomations": "另有 {{count}} 個",
|
||||
"confirmWithAutomations": "刪除對話和自動任務",
|
||||
"confirmWithAutomations": "刪除",
|
||||
"schedule": {
|
||||
"at": "{{time}}",
|
||||
"every": "每 {{duration}}",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type {
|
||||
AutomationsPayload,
|
||||
AutomationUpdatePayload,
|
||||
ChatSummary,
|
||||
CliAppsPayload,
|
||||
FilePreviewPayload,
|
||||
@@ -87,6 +89,10 @@ function mcpValuesHeader(values: Record<string, unknown>): HeadersInit | undefin
|
||||
return { "X-Nanobot-MCP-Values": JSON.stringify(payload) };
|
||||
}
|
||||
|
||||
function automationValuesHeader(values: AutomationUpdatePayload): HeadersInit {
|
||||
return { "X-Nanobot-Automation-Values": encodeURIComponent(JSON.stringify(values)) };
|
||||
}
|
||||
|
||||
function splitKey(key: string): { channel: string; chatId: string } {
|
||||
const idx = key.indexOf(":");
|
||||
if (idx === -1) return { channel: "", chatId: key };
|
||||
@@ -184,6 +190,52 @@ export async function fetchSessionAutomations(
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchAutomations(
|
||||
token: string,
|
||||
base: string = "",
|
||||
): Promise<AutomationsPayload> {
|
||||
return request<AutomationsPayload>(
|
||||
`${base}/api/webui/automations`,
|
||||
token,
|
||||
undefined,
|
||||
API_READ_TIMEOUT_MS,
|
||||
);
|
||||
}
|
||||
|
||||
export async function runAutomationAction(
|
||||
token: string,
|
||||
action: "enable" | "disable" | "delete" | "run",
|
||||
id: string,
|
||||
base: string = "",
|
||||
): Promise<AutomationsPayload> {
|
||||
const query = new URLSearchParams();
|
||||
query.set("id", id);
|
||||
return request<AutomationsPayload>(
|
||||
`${base}/api/webui/automations/${action}?${query}`,
|
||||
token,
|
||||
undefined,
|
||||
API_READ_TIMEOUT_MS,
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateAutomation(
|
||||
token: string,
|
||||
id: string,
|
||||
values: AutomationUpdatePayload,
|
||||
base: string = "",
|
||||
): Promise<AutomationsPayload> {
|
||||
const query = new URLSearchParams();
|
||||
query.set("id", id);
|
||||
return request<AutomationsPayload>(
|
||||
`${base}/api/webui/automations/update?${query}`,
|
||||
token,
|
||||
{
|
||||
headers: automationValuesHeader(values),
|
||||
},
|
||||
API_READ_TIMEOUT_MS,
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchSkills(
|
||||
token: string,
|
||||
base: string = "",
|
||||
|
||||
@@ -100,6 +100,10 @@ export interface SessionAutomationJob {
|
||||
id: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
protected?: boolean;
|
||||
delete_after_run?: boolean;
|
||||
created_at_ms?: number | null;
|
||||
updated_at_ms?: number | null;
|
||||
schedule: {
|
||||
kind: "at" | "every" | "cron" | string;
|
||||
at_ms?: number | null;
|
||||
@@ -109,15 +113,43 @@ export interface SessionAutomationJob {
|
||||
};
|
||||
payload: {
|
||||
message: string;
|
||||
kind?: "agent_turn" | "system_event" | string;
|
||||
};
|
||||
state: {
|
||||
next_run_at_ms?: number | null;
|
||||
last_run_at_ms?: number | null;
|
||||
last_status?: "ok" | "error" | "skipped" | string | null;
|
||||
last_error?: string | null;
|
||||
pending?: boolean;
|
||||
run_history?: Array<{
|
||||
run_at_ms: number;
|
||||
status: "ok" | "error" | "skipped" | string;
|
||||
duration_ms?: number;
|
||||
error?: string | null;
|
||||
}>;
|
||||
};
|
||||
origin?: {
|
||||
session_key?: string;
|
||||
channel: string;
|
||||
chat_id?: string;
|
||||
title?: string;
|
||||
preview?: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface SessionAutomationsPayload { jobs: SessionAutomationJob[]; }
|
||||
export interface AutomationsPayload { jobs: SessionAutomationJob[]; }
|
||||
export interface AutomationUpdatePayload {
|
||||
name?: string;
|
||||
message?: string;
|
||||
schedule?: {
|
||||
kind: "at" | "every" | "cron";
|
||||
at_ms?: number;
|
||||
every_ms?: number;
|
||||
expr?: string;
|
||||
tz?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SessionDeleteResult {
|
||||
deleted: boolean;
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
createModelConfiguration,
|
||||
deleteSession,
|
||||
fetchFilePreview,
|
||||
fetchAutomations,
|
||||
fetchCliApps,
|
||||
fetchInstalledCliApps,
|
||||
fetchMcpPresets,
|
||||
@@ -20,9 +21,11 @@ import {
|
||||
listSlashCommands,
|
||||
loginProviderOAuth,
|
||||
logoutProviderOAuth,
|
||||
runAutomationAction,
|
||||
runCliAppAction,
|
||||
runMcpPresetAction,
|
||||
saveCustomMcpServer,
|
||||
updateAutomation,
|
||||
updateSidebarState,
|
||||
updateImageGenerationSettings,
|
||||
updateModelConfiguration,
|
||||
@@ -99,6 +102,49 @@ describe("webui API helpers", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("fetches workspace automations", async () => {
|
||||
await fetchAutomations("tok");
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"/api/webui/automations",
|
||||
expect.objectContaining({
|
||||
headers: { Authorization: "Bearer tok" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("serializes workspace automation actions", async () => {
|
||||
await runAutomationAction("tok", "disable", "job 1/2");
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"/api/webui/automations/disable?id=job+1%2F2",
|
||||
expect.objectContaining({
|
||||
headers: { Authorization: "Bearer tok" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("serializes workspace automation updates", async () => {
|
||||
const values = {
|
||||
name: "每日测验",
|
||||
message: "Ask 今日 quiz",
|
||||
schedule: { kind: "cron", expr: "0 9 * * *", tz: "Asia/Shanghai" },
|
||||
} as const;
|
||||
await updateAutomation("tok", "job 1/2", values);
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"/api/webui/automations/update?id=job+1%2F2",
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
Authorization: "Bearer tok",
|
||||
"X-Nanobot-Automation-Values": encodeURIComponent(JSON.stringify(values)),
|
||||
},
|
||||
}),
|
||||
);
|
||||
const header = vi.mocked(fetch).mock.calls[0][1]?.headers as Record<string, string>;
|
||||
expect(header["X-Nanobot-Automation-Values"]).not.toContain("每日");
|
||||
});
|
||||
|
||||
it("fetches the WebUI skill summary", async () => {
|
||||
await fetchSkills("tok");
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ const toggleThemeSpy = vi.fn();
|
||||
const updateUrlSpy = vi.fn();
|
||||
const attachSpy = vi.fn();
|
||||
const runStatusHandlers = new Set<(chatId: string, startedAt: number | null) => void>();
|
||||
const sessionUpdateHandlers = new Set<(chatId: string, scope?: string) => void>();
|
||||
let mockSessions: ChatSummary[] = [];
|
||||
const HERO_GREETING_PATTERN =
|
||||
/What should we work on\?|Where should we start\?|What are we building today\?|What should we tackle together\?/;
|
||||
@@ -194,7 +195,10 @@ vi.mock("@/lib/nanobot-client", () => {
|
||||
onRuntimeModelUpdate = () => () => {};
|
||||
onError = () => () => {};
|
||||
onChat = () => () => {};
|
||||
onSessionUpdate = () => () => {};
|
||||
onSessionUpdate = (handler: (chatId: string, scope?: string) => void) => {
|
||||
sessionUpdateHandlers.add(handler);
|
||||
return () => sessionUpdateHandlers.delete(handler);
|
||||
};
|
||||
onRunStatus = (handler: (chatId: string, startedAt: number | null) => void) => {
|
||||
runStatusHandlers.add(handler);
|
||||
return () => runStatusHandlers.delete(handler);
|
||||
@@ -227,10 +231,12 @@ describe("App layout", () => {
|
||||
toggleThemeSpy.mockReset();
|
||||
attachSpy.mockReset();
|
||||
runStatusHandlers.clear();
|
||||
sessionUpdateHandlers.clear();
|
||||
window.history.replaceState(null, "", "/");
|
||||
setNavigatorPlatform("Linux x86_64");
|
||||
localStorage.removeItem("nanobot-webui.sidebar");
|
||||
localStorage.removeItem("nanobot-webui.sidebar.completed-runs.v1");
|
||||
localStorage.removeItem("nanobot-webui.sidebar.session-updates.v1");
|
||||
vi.mocked(fetchBootstrap).mockReset().mockResolvedValue({
|
||||
token: "tok",
|
||||
ws_path: "/",
|
||||
@@ -265,6 +271,23 @@ describe("App layout", () => {
|
||||
expect(asideClassNames.some((cls) => cls.includes("lg:block"))).toBe(true);
|
||||
});
|
||||
|
||||
it("places Automations after Skills in the main sidebar", async () => {
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => expect(connectSpy).toHaveBeenCalled());
|
||||
const sidebar = screen.getByRole("navigation", { name: "Sidebar navigation" });
|
||||
const appsButton = within(sidebar).getByRole("button", { name: "Apps" });
|
||||
const skillsButton = within(sidebar).getByRole("button", { name: "Skills" });
|
||||
const automationsButton = within(sidebar).getByRole("button", { name: "Automations" });
|
||||
|
||||
expect(appsButton.compareDocumentPosition(skillsButton) & Node.DOCUMENT_POSITION_FOLLOWING)
|
||||
.toBeTruthy();
|
||||
expect(
|
||||
skillsButton.compareDocumentPosition(automationsButton) &
|
||||
Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("opens Skills from the main sidebar", async () => {
|
||||
mockFetchRoutes({
|
||||
"/api/settings": baseSettingsPayload(),
|
||||
@@ -334,6 +357,331 @@ describe("App layout", () => {
|
||||
expect(screen.getByText(/Use GitHub CLI/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens Automations from the main sidebar", async () => {
|
||||
mockFetchRoutes({
|
||||
"/api/settings": baseSettingsPayload(),
|
||||
"/api/webui/automations": {
|
||||
jobs: [
|
||||
{
|
||||
id: "job-1",
|
||||
name: "Daily repo check",
|
||||
enabled: true,
|
||||
protected: false,
|
||||
delete_after_run: false,
|
||||
schedule: { kind: "every", every_ms: 86_400_000 },
|
||||
payload: {
|
||||
message: "Check the repo status",
|
||||
kind: "agent_turn",
|
||||
},
|
||||
state: {
|
||||
next_run_at_ms: Date.UTC(2026, 3, 17, 10, 0, 0),
|
||||
last_status: "ok",
|
||||
pending: false,
|
||||
run_history: [],
|
||||
},
|
||||
origin: {
|
||||
session_key: "websocket:chat-a",
|
||||
channel: "websocket",
|
||||
chat_id: "chat-a",
|
||||
title: "Release prep",
|
||||
preview: "Check release blockers",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "external-quiz",
|
||||
name: "WeChat quiz",
|
||||
enabled: true,
|
||||
protected: false,
|
||||
delete_after_run: false,
|
||||
schedule: { kind: "cron", expr: "30 9-23 * * *", tz: "Asia/Shanghai" },
|
||||
payload: {
|
||||
message: "Send a quiz",
|
||||
kind: "agent_turn",
|
||||
},
|
||||
state: {
|
||||
next_run_at_ms: Date.UTC(2026, 3, 17, 11, 30, 0),
|
||||
last_status: "ok",
|
||||
pending: false,
|
||||
run_history: [],
|
||||
},
|
||||
origin: {
|
||||
channel: "weixin",
|
||||
title: "",
|
||||
preview: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "heartbeat",
|
||||
name: "heartbeat",
|
||||
enabled: true,
|
||||
protected: true,
|
||||
schedule: { kind: "every", every_ms: 60_000 },
|
||||
payload: { message: "", kind: "system_event" },
|
||||
state: { next_run_at_ms: null, pending: false, run_history: [] },
|
||||
origin: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => expect(connectSpy).toHaveBeenCalled());
|
||||
const sidebar = screen.getByRole("navigation", { name: "Sidebar navigation" });
|
||||
const automationsButton = within(sidebar).getByRole("button", {
|
||||
name: "Automations",
|
||||
});
|
||||
|
||||
fireEvent.click(automationsButton);
|
||||
|
||||
const heading = await screen.findByRole("heading", { name: "Automations" });
|
||||
expect(heading).toBeInTheDocument();
|
||||
const automationsMain = heading.closest("main");
|
||||
expect(automationsMain).not.toBeNull();
|
||||
expect(within(automationsMain as HTMLElement).queryByText("Settings")).not.toBeInTheDocument();
|
||||
expect(screen.getAllByText("Daily repo check").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("Check the repo status").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("Release prep").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getByText("WeChat quiz")).toBeInTheDocument();
|
||||
expect(screen.getByText("WeChat")).toBeInTheDocument();
|
||||
expect(screen.queryByText("weixin:wx-chat")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("memory with dream state")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("heartbeat")).toBeInTheDocument();
|
||||
expect(within(sidebar).getByRole("button", { name: "Automations" })).toHaveAttribute(
|
||||
"aria-current",
|
||||
"page",
|
||||
);
|
||||
expect(document.title).toBe("Automations · nanobot");
|
||||
|
||||
const searchInput = within(automationsMain as HTMLElement).getByPlaceholderText(
|
||||
"Search task, message, linked chat, or schedule",
|
||||
);
|
||||
fireEvent.change(searchInput, { target: { value: "WeChat" } });
|
||||
await waitFor(() => expect(screen.queryByText("Daily repo check")).not.toBeInTheDocument());
|
||||
expect(screen.getAllByText("WeChat quiz").length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
fireEvent.change(searchInput, { target: { value: "09-23" } });
|
||||
await waitFor(() => expect(screen.queryByText("Daily repo check")).not.toBeInTheDocument());
|
||||
expect(screen.getAllByText("WeChat quiz").length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("edits a past one-time automation without resubmitting its old schedule", async () => {
|
||||
const pastOneShot = {
|
||||
id: "past-one-shot",
|
||||
name: "Past one-shot",
|
||||
enabled: true,
|
||||
protected: false,
|
||||
delete_after_run: true,
|
||||
schedule: { kind: "at", at_ms: 1 },
|
||||
payload: {
|
||||
message: "Old one-shot message",
|
||||
kind: "agent_turn",
|
||||
},
|
||||
state: {
|
||||
next_run_at_ms: null,
|
||||
last_status: "ok",
|
||||
pending: false,
|
||||
run_history: [],
|
||||
},
|
||||
origin: {
|
||||
session_key: "websocket:chat-a",
|
||||
channel: "websocket",
|
||||
chat_id: "chat-a",
|
||||
title: "Release prep",
|
||||
preview: "Check release blockers",
|
||||
},
|
||||
};
|
||||
mockFetchRoutes({
|
||||
"/api/settings": baseSettingsPayload(),
|
||||
"/api/webui/automations": { jobs: [pastOneShot] },
|
||||
"/api/webui/automations/update?id=past-one-shot": {
|
||||
jobs: [
|
||||
{
|
||||
...pastOneShot,
|
||||
payload: { ...pastOneShot.payload, message: "Updated one-shot message" },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => expect(connectSpy).toHaveBeenCalled());
|
||||
const sidebar = screen.getByRole("navigation", { name: "Sidebar navigation" });
|
||||
fireEvent.click(within(sidebar).getByRole("button", { name: "Automations" }));
|
||||
|
||||
expect((await screen.findAllByText("Past one-shot")).length).toBeGreaterThanOrEqual(1);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Edit" }));
|
||||
expect(screen.queryByText("Run time must be in the future.")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("Update the prompt and schedule. The linked chat stays unchanged."),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("Old one-shot message")).toHaveClass(
|
||||
"min-h-[160px]",
|
||||
"resize-none",
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByDisplayValue("Old one-shot message"), {
|
||||
target: { value: "Updated one-shot message" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"/api/webui/automations/update?id=past-one-shot",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
const updateCall = vi.mocked(fetch).mock.calls.find(
|
||||
([url]) => String(url) === "/api/webui/automations/update?id=past-one-shot",
|
||||
);
|
||||
expect(updateCall).toBeTruthy();
|
||||
const headers = updateCall?.[1]?.headers as Record<string, string>;
|
||||
expect(JSON.parse(decodeURIComponent(headers["X-Nanobot-Automation-Values"]))).toEqual({
|
||||
name: "Past one-shot",
|
||||
message: "Updated one-shot message",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps long automation details expandable without nested scrolling", async () => {
|
||||
const longMessage = [
|
||||
"Review the release plan and prepare a concise status update for the channel.",
|
||||
"Include blockers, owners, follow-up dates, and any risky assumptions that changed since yesterday.",
|
||||
"Keep the output actionable and avoid repeating context that the team already confirmed in the thread.",
|
||||
"If a dependency looks stale, call it out explicitly and ask for a fresh owner update.",
|
||||
"This message is intentionally long enough to require progressive disclosure in the automation details panel.",
|
||||
"The full content should remain available without forcing the user into a small nested scroll area.",
|
||||
].join("\n");
|
||||
const history = [
|
||||
{ run_at_ms: Date.UTC(2026, 3, 12, 10, 0, 0), status: "error", duration_ms: 900, error: "oldest failure" },
|
||||
{ run_at_ms: Date.UTC(2026, 3, 13, 10, 0, 0), status: "error", duration_ms: 800, error: "second oldest failure" },
|
||||
{ run_at_ms: Date.UTC(2026, 3, 14, 10, 0, 0), status: "ok", duration_ms: 700 },
|
||||
{ run_at_ms: Date.UTC(2026, 3, 15, 10, 0, 0), status: "ok", duration_ms: 600 },
|
||||
{ run_at_ms: Date.UTC(2026, 3, 16, 10, 0, 0), status: "ok", duration_ms: 500 },
|
||||
{ run_at_ms: Date.UTC(2026, 3, 17, 10, 0, 0), status: "ok", duration_ms: 400 },
|
||||
];
|
||||
mockFetchRoutes({
|
||||
"/api/settings": baseSettingsPayload(),
|
||||
"/api/webui/automations": {
|
||||
jobs: [
|
||||
{
|
||||
id: "long-details",
|
||||
name: "Long detail automation",
|
||||
enabled: true,
|
||||
protected: false,
|
||||
delete_after_run: false,
|
||||
schedule: { kind: "every", every_ms: 3_600_000 },
|
||||
payload: {
|
||||
message: longMessage,
|
||||
kind: "agent_turn",
|
||||
},
|
||||
state: {
|
||||
next_run_at_ms: Date.UTC(2026, 3, 18, 10, 0, 0),
|
||||
last_status: "ok",
|
||||
pending: false,
|
||||
run_history: history,
|
||||
},
|
||||
origin: {
|
||||
session_key: "websocket:chat-a",
|
||||
channel: "websocket",
|
||||
chat_id: "chat-a",
|
||||
title: "Release prep",
|
||||
preview: "Check release blockers",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => expect(connectSpy).toHaveBeenCalled());
|
||||
const sidebar = screen.getByRole("navigation", { name: "Sidebar navigation" });
|
||||
fireEvent.click(within(sidebar).getByRole("button", { name: "Automations" }));
|
||||
|
||||
const detailHeading = await screen.findByRole("heading", { name: "Long detail automation" });
|
||||
const detailPanel = detailHeading.closest("article") as HTMLElement;
|
||||
expect(detailPanel).not.toBeNull();
|
||||
const message = Array.from(detailPanel.querySelectorAll("section div")).find(
|
||||
(node) => node.textContent === longMessage,
|
||||
) as HTMLElement | undefined;
|
||||
expect(message).toBeTruthy();
|
||||
expect(message!).toHaveClass("line-clamp-6");
|
||||
|
||||
fireEvent.click(within(detailPanel).getByRole("button", { name: "Show full message" }));
|
||||
expect(within(detailPanel).getByRole("button", { name: "Show less" })).toBeInTheDocument();
|
||||
expect(message!).not.toHaveClass("line-clamp-6");
|
||||
|
||||
expect(within(detailPanel).queryByText("Recent health")).not.toBeInTheDocument();
|
||||
expect(within(detailPanel).queryByRole("button", { name: /Run history/ })).not.toBeInTheDocument();
|
||||
expect(within(detailPanel).queryByText(/oldest failure/)).not.toBeInTheDocument();
|
||||
expect(within(detailPanel).queryByText("No error recorded")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("localizes the Automations surface", async () => {
|
||||
await i18n.changeLanguage("zh-CN");
|
||||
mockFetchRoutes({
|
||||
"/api/settings": baseSettingsPayload(),
|
||||
"/api/webui/automations": {
|
||||
jobs: [
|
||||
{
|
||||
id: "job-zh",
|
||||
name: "每日检查",
|
||||
enabled: true,
|
||||
protected: false,
|
||||
delete_after_run: false,
|
||||
schedule: { kind: "every", every_ms: 86_400_000 },
|
||||
payload: {
|
||||
message: "检查仓库状态",
|
||||
kind: "agent_turn",
|
||||
},
|
||||
state: {
|
||||
next_run_at_ms: Date.UTC(2026, 3, 17, 10, 0, 0),
|
||||
last_run_at_ms: Date.UTC(2026, 3, 16, 10, 0, 0),
|
||||
last_status: "ok",
|
||||
pending: false,
|
||||
run_history: [
|
||||
{
|
||||
run_at_ms: Date.UTC(2026, 3, 16, 10, 0, 0),
|
||||
status: "ok",
|
||||
duration_ms: 500,
|
||||
},
|
||||
],
|
||||
},
|
||||
origin: {
|
||||
session_key: "websocket:chat-a",
|
||||
channel: "websocket",
|
||||
chat_id: "chat-a",
|
||||
title: "发布准备",
|
||||
preview: "检查发布阻塞项",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => expect(connectSpy).toHaveBeenCalled());
|
||||
const sidebar = screen.getByRole("navigation", { name: "侧边栏导航" });
|
||||
fireEvent.click(within(sidebar).getByRole("button", { name: "自动任务" }));
|
||||
|
||||
const heading = await screen.findByRole("heading", { name: "自动任务" });
|
||||
expect(heading).toBeInTheDocument();
|
||||
const automationsMain = heading.closest("main");
|
||||
expect(automationsMain).not.toBeNull();
|
||||
expect(within(automationsMain as HTMLElement).queryByText("设置")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("任务队列")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("每日检查").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("检查仓库状态").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getByText("每 1天")).toBeInTheDocument();
|
||||
expect(screen.queryByText("最近健康状态")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("近期无问题")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Workspace automations")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "刷新" })).not.toBeInTheDocument();
|
||||
expect(document.title).toBe("自动任务 · nanobot");
|
||||
});
|
||||
|
||||
it("fully collapses the native host sidebar and previews it on hover", async () => {
|
||||
mockSessions = [
|
||||
{
|
||||
@@ -497,7 +845,7 @@ describe("App layout", () => {
|
||||
screen.queryByText("This chat has scheduled automations. Deleting it will also delete them."),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "删除对话和自动任务" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "删除" }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(deleteChatSpy).toHaveBeenCalledWith("websocket:chat-a", {
|
||||
@@ -754,15 +1102,15 @@ describe("App layout", () => {
|
||||
for (const handler of runStatusHandlers) handler("chat-a", null);
|
||||
});
|
||||
expect(within(sidebar).queryByTitle("Agent running")).not.toBeInTheDocument();
|
||||
expect(within(sidebar).getByTitle("Agent finished")).toBeInTheDocument();
|
||||
expect(within(sidebar).getByTitle("New activity")).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(within(sidebar).getByRole("button", { name: /^Working chat$/ }));
|
||||
});
|
||||
expect(within(sidebar).queryByTitle("Agent finished")).not.toBeInTheDocument();
|
||||
expect(within(sidebar).queryByTitle("New activity")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not show a completed dot later when the active session finishes", async () => {
|
||||
it("does not show an updated dot later when the active session finishes", async () => {
|
||||
mockSessions = [
|
||||
{
|
||||
key: "websocket:chat-a",
|
||||
@@ -806,12 +1154,53 @@ describe("App layout", () => {
|
||||
for (const handler of runStatusHandlers) handler("chat-a", null);
|
||||
});
|
||||
expect(within(sidebar).queryByTitle("Agent running")).not.toBeInTheDocument();
|
||||
expect(within(sidebar).queryByTitle("Agent finished")).not.toBeInTheDocument();
|
||||
expect(within(sidebar).queryByTitle("New activity")).not.toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(within(sidebar).getByRole("button", { name: /^Other chat$/ }));
|
||||
});
|
||||
expect(within(sidebar).queryByTitle("Agent finished")).not.toBeInTheDocument();
|
||||
expect(within(sidebar).queryByTitle("New activity")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("marks inactive sessions when a thread update arrives", async () => {
|
||||
mockSessions = [
|
||||
{
|
||||
key: "websocket:chat-a",
|
||||
channel: "websocket",
|
||||
chatId: "chat-a",
|
||||
createdAt: "2026-04-16T10:00:00Z",
|
||||
updatedAt: "2026-04-16T10:00:00Z",
|
||||
preview: "Open chat",
|
||||
},
|
||||
{
|
||||
key: "websocket:chat-b",
|
||||
channel: "websocket",
|
||||
chatId: "chat-b",
|
||||
createdAt: "2026-04-16T11:00:00Z",
|
||||
updatedAt: "2026-04-16T11:00:00Z",
|
||||
preview: "Scheduled update target",
|
||||
},
|
||||
];
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => expect(connectSpy).toHaveBeenCalled());
|
||||
const sidebar = screen.getByRole("navigation", { name: "Sidebar navigation" });
|
||||
await act(async () => {
|
||||
fireEvent.click(within(sidebar).getByRole("button", { name: /^Open chat$/ }));
|
||||
});
|
||||
|
||||
act(() => {
|
||||
for (const handler of sessionUpdateHandlers) handler("chat-b", "thread");
|
||||
});
|
||||
|
||||
expect(within(sidebar).getByTitle("New activity")).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(within(sidebar).getByRole("button", { name: /^Scheduled update target$/ }));
|
||||
});
|
||||
|
||||
expect(within(sidebar).queryByTitle("New activity")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("restores sidebar run indicators after a page reload", async () => {
|
||||
@@ -835,7 +1224,7 @@ describe("App layout", () => {
|
||||
},
|
||||
];
|
||||
localStorage.setItem(
|
||||
"nanobot-webui.sidebar.completed-runs.v1",
|
||||
"nanobot-webui.sidebar.session-updates.v1",
|
||||
JSON.stringify(["chat-b"]),
|
||||
);
|
||||
|
||||
@@ -846,7 +1235,7 @@ describe("App layout", () => {
|
||||
await waitFor(() =>
|
||||
expect(within(sidebar).getByTitle("Agent running")).toBeInTheDocument(),
|
||||
);
|
||||
expect(within(sidebar).getByTitle("Agent finished")).toBeInTheDocument();
|
||||
expect(within(sidebar).getByTitle("New activity")).toBeInTheDocument();
|
||||
expect(attachSpy).toHaveBeenCalledWith("chat-a");
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,44 @@ function session(overrides: Partial<ChatSummary>): ChatSummary {
|
||||
}
|
||||
|
||||
describe("ChatList", () => {
|
||||
it("orders chats by latest session activity by default", () => {
|
||||
const sessions = [
|
||||
session({
|
||||
chatId: "older",
|
||||
title: "Older chat",
|
||||
updatedAt: "2026-05-21T10:00:00Z",
|
||||
}),
|
||||
session({
|
||||
chatId: "newest",
|
||||
title: "Newest chat",
|
||||
updatedAt: "2026-05-21T12:00:00Z",
|
||||
}),
|
||||
session({
|
||||
chatId: "middle",
|
||||
title: "Middle chat",
|
||||
updatedAt: "2026-05-21T11:00:00Z",
|
||||
}),
|
||||
];
|
||||
|
||||
render(
|
||||
<ChatList
|
||||
sessions={sessions}
|
||||
activeKey={null}
|
||||
onSelect={vi.fn()}
|
||||
onRequestDelete={vi.fn()}
|
||||
onTogglePin={vi.fn()}
|
||||
onRequestRename={vi.fn()}
|
||||
onToggleArchive={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const chatsSection = screen.getAllByRole("region")[0];
|
||||
const text = chatsSection.textContent ?? "";
|
||||
|
||||
expect(text.indexOf("Newest chat")).toBeLessThan(text.indexOf("Middle chat"));
|
||||
expect(text.indexOf("Middle chat")).toBeLessThan(text.indexOf("Older chat"));
|
||||
});
|
||||
|
||||
it("groups WebUI chats by workspace project while preserving in-project sorting and activity", () => {
|
||||
const sessions = [
|
||||
session({
|
||||
@@ -179,7 +217,7 @@ describe("ChatList", () => {
|
||||
expect(onRequestRenameProject).toHaveBeenCalledWith("/Users/me/nanobot", "Photos");
|
||||
});
|
||||
|
||||
it("hides the completed dot for the active chat", () => {
|
||||
it("hides the updated dot for the active chat", () => {
|
||||
const sessions = [
|
||||
session({
|
||||
chatId: "active",
|
||||
@@ -200,13 +238,13 @@ describe("ChatList", () => {
|
||||
onTogglePin={vi.fn()}
|
||||
onRequestRename={vi.fn()}
|
||||
onToggleArchive={vi.fn()}
|
||||
completedChatIds={["active", "done"]}
|
||||
updatedChatIds={["active", "done"]}
|
||||
/>,
|
||||
);
|
||||
|
||||
const finished = screen.getAllByLabelText("Agent finished");
|
||||
expect(finished).toHaveLength(1);
|
||||
expect(finished[0].firstElementChild).toHaveClass("h-2", "w-2");
|
||||
const updated = screen.getAllByLabelText("New activity");
|
||||
expect(updated).toHaveLength(1);
|
||||
expect(updated[0].firstElementChild).toHaveClass("h-2", "w-2");
|
||||
});
|
||||
|
||||
it("folds long default workspace chats and can show all", () => {
|
||||
|
||||
@@ -31,6 +31,7 @@ const SETTINGS_NAV_KEYS = [
|
||||
"image",
|
||||
"browser",
|
||||
"apps",
|
||||
"automations",
|
||||
"runtime",
|
||||
"advanced",
|
||||
];
|
||||
@@ -43,8 +44,17 @@ const LOCALIZED_SETTINGS_COPY_KEYS = [
|
||||
"settings.nav.models",
|
||||
"settings.nav.providers",
|
||||
"settings.nav.apps",
|
||||
"settings.nav.automations",
|
||||
"settings.nav.runtime",
|
||||
"settings.nav.advanced",
|
||||
"sidebar.automations",
|
||||
"settings.automations.filters.active",
|
||||
"settings.automations.queue",
|
||||
"settings.automations.empty",
|
||||
"settings.automations.systemTask",
|
||||
"settings.automations.labels.schedule",
|
||||
"settings.automations.status.active",
|
||||
"settings.automations.deleteTitle",
|
||||
"settings.sections.interface",
|
||||
"settings.sections.localPreferences",
|
||||
"settings.sections.webSearch",
|
||||
|
||||
@@ -159,8 +159,9 @@ const installedAnyGen = {
|
||||
|
||||
function renderSettingsView(
|
||||
options: {
|
||||
initialSection?: "overview" | "apps" | "advanced" | "models";
|
||||
initialSection?: "overview" | "apps" | "automations" | "advanced" | "models";
|
||||
initialSettings?: SettingsPayload;
|
||||
showSidebar?: boolean;
|
||||
onSettingsChange?: (payload: SettingsPayload) => void;
|
||||
onNativeEngineRestart?: () => Promise<string>;
|
||||
} = {},
|
||||
@@ -171,6 +172,7 @@ function renderSettingsView(
|
||||
theme="light"
|
||||
initialSection={options.initialSection ?? "apps"}
|
||||
initialSettings={options.initialSettings}
|
||||
showSidebar={options.showSidebar}
|
||||
onToggleTheme={() => {}}
|
||||
onBackToChat={() => {}}
|
||||
onModelNameChange={() => {}}
|
||||
@@ -187,6 +189,25 @@ describe("SettingsView Apps catalog", () => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("does not show the Settings kicker on the standalone Automations surface", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url === "/api/settings") return jsonResponse(settingsPayload());
|
||||
if (url === "/api/webui/automations") return jsonResponse({ jobs: [] });
|
||||
return jsonResponse({});
|
||||
}));
|
||||
|
||||
renderSettingsView({
|
||||
initialSection: "automations",
|
||||
initialSettings: settingsPayload(),
|
||||
showSidebar: false,
|
||||
});
|
||||
|
||||
expect(screen.getByRole("heading", { name: "Automations" })).toBeInTheDocument();
|
||||
expect(await screen.findByText("No automations yet.")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Settings")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows a visible uninstall button for installed CLI apps and calls uninstall", async () => {
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
|
||||
Reference in New Issue
Block a user