feat(cli): TUI grid threshold, narrow agents view, new-task popup, browser launch
- Lower the multi-panel grid threshold from 100×24 to 80×20 so standard terminals render the full layout instead of falling back to single-pane. - Agents view stacks list above detail when cols < 80; Tab focus model preserved. - Board view: press [n] to open a centered create-task popup. Title is edited via ink-text-input; submit calls TaskStore.createTask, refresh the task list on success. - System panel: shows the tokenized auth URL alongside the token, says "no auth" plainly when --no-auth was used, and prompts with "Press [Enter] to open in browser". Enter (while System is focused) spawns the platform's URL opener (open / start / xdg-open). - Tighten log-row layout: each row is now a single Text with inline spans + fixed-width prefix slot so columns align and the highlight bar spans the full row. - Default status section is "logs" so the live feed is what users see when they run `fn`. - Drop the ASCII circle brand mark from the splash; FUSION block letters + tagline + spinner only, pinned to the top-left. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,12 @@ function makeInteractiveData(opts: {
|
|||||||
return {
|
return {
|
||||||
listProjects: async () => projects,
|
listProjects: async () => projects,
|
||||||
listTasks: async () => tasks,
|
listTasks: async () => tasks,
|
||||||
|
createTask: async (_path: string, input: { title: string; description?: string }) => ({
|
||||||
|
id: "new",
|
||||||
|
title: input.title,
|
||||||
|
description: input.description ?? input.title,
|
||||||
|
column: "todo",
|
||||||
|
}) as TaskItem,
|
||||||
listAgents: async () => agents,
|
listAgents: async () => agents,
|
||||||
getAgentDetail: async (_id: string) => detail,
|
getAgentDetail: async (_id: string) => detail,
|
||||||
updateAgentState: async (_id: string, _state: string) => {},
|
updateAgentState: async (_id: string, _state: string) => {},
|
||||||
|
|||||||
@@ -1,6 +1,32 @@
|
|||||||
import React, { useState, useSyncExternalStore, useCallback, useEffect } from "react";
|
import React, { useState, useSyncExternalStore, useCallback, useEffect } from "react";
|
||||||
import { Box, Text, useInput, useApp, useStdout } from "ink";
|
import { Box, Text, useInput, useApp, useStdout } from "ink";
|
||||||
import Spinner from "ink-spinner";
|
import Spinner from "ink-spinner";
|
||||||
|
import TextInput from "ink-text-input";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
|
||||||
|
// Open a URL in the user's default browser. Uses the platform-native opener
|
||||||
|
// (macOS `open`, Windows `start`, Linux `xdg-open`). Detached + ignored stdio
|
||||||
|
// so the spawned process doesn't block the TUI's input loop.
|
||||||
|
function openInBrowser(url: string): void {
|
||||||
|
let cmd: string;
|
||||||
|
let args: string[];
|
||||||
|
if (process.platform === "darwin") {
|
||||||
|
cmd = "open";
|
||||||
|
args = [url];
|
||||||
|
} else if (process.platform === "win32") {
|
||||||
|
cmd = "cmd";
|
||||||
|
args = ["/c", "start", "", url];
|
||||||
|
} else {
|
||||||
|
cmd = "xdg-open";
|
||||||
|
args = [url];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
|
||||||
|
child.unref();
|
||||||
|
} catch {
|
||||||
|
// Best-effort — silently ignore if the platform tool isn't available.
|
||||||
|
}
|
||||||
|
}
|
||||||
import type { DashboardTUI } from "./controller.js";
|
import type { DashboardTUI } from "./controller.js";
|
||||||
import type {
|
import type {
|
||||||
DashboardState,
|
DashboardState,
|
||||||
@@ -170,20 +196,38 @@ function SystemPanel({ state, isFocused }: { state: DashboardState; isFocused: b
|
|||||||
<>
|
<>
|
||||||
<Box flexDirection="row" gap={1}>
|
<Box flexDirection="row" gap={1}>
|
||||||
<Text dimColor>Auth:</Text>
|
<Text dimColor>Auth:</Text>
|
||||||
<Text color="yellow">bearer token</Text>
|
<Text color="yellow">bearer token required</Text>
|
||||||
</Box>
|
</Box>
|
||||||
{info.authToken && (
|
{info.authToken && (
|
||||||
<Box flexDirection="row" gap={1}>
|
<Box flexDirection="row" gap={1}>
|
||||||
<Text dimColor>Token:</Text>
|
<Text dimColor>Token:</Text>
|
||||||
<Text wrap="truncate">{info.authToken}</Text>
|
<Text wrap="truncate" color="yellow">{info.authToken}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
{info.tokenizedUrl && (
|
||||||
|
<Box flexDirection="row" gap={1}>
|
||||||
|
<Text dimColor>Open:</Text>
|
||||||
|
<Text wrap="truncate" color="cyanBright">{info.tokenizedUrl}</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box flexDirection="row" gap={1}>
|
||||||
|
<Text dimColor>Press</Text>
|
||||||
|
<Text color="cyanBright" bold>[Enter]</Text>
|
||||||
|
<Text dimColor>to open in browser</Text>
|
||||||
|
</Box>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Box flexDirection="row" gap={1}>
|
<>
|
||||||
<Text dimColor>Auth:</Text>
|
<Box flexDirection="row" gap={1}>
|
||||||
<Text dimColor>disabled</Text>
|
<Text dimColor>Auth:</Text>
|
||||||
</Box>
|
<Text color="yellow">no auth</Text>
|
||||||
|
</Box>
|
||||||
|
<Box flexDirection="row" gap={1}>
|
||||||
|
<Text dimColor>Press</Text>
|
||||||
|
<Text color="cyanBright" bold>[Enter]</Text>
|
||||||
|
<Text dimColor>to open in browser</Text>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<Box flexDirection="row" gap={1}>
|
<Box flexDirection="row" gap={1}>
|
||||||
<Text dimColor>Engine:</Text>
|
<Text dimColor>Engine:</Text>
|
||||||
@@ -833,7 +877,7 @@ function TaskDetailScreen({ task }: { task: TaskItem }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type BoardSubView = "board" | "detail" | "picker";
|
type BoardSubView = "board" | "detail" | "picker" | "create";
|
||||||
|
|
||||||
function clamp(n: number, min: number, max: number): number {
|
function clamp(n: number, min: number, max: number): number {
|
||||||
return Math.max(min, Math.min(max, n));
|
return Math.max(min, Math.min(max, n));
|
||||||
@@ -870,6 +914,9 @@ function BoardView({ state }: { state: DashboardState }) {
|
|||||||
done: 0,
|
done: 0,
|
||||||
});
|
});
|
||||||
const [pickerOriginal, setPickerOriginal] = useState(0);
|
const [pickerOriginal, setPickerOriginal] = useState(0);
|
||||||
|
const [newTaskTitle, setNewTaskTitle] = useState("");
|
||||||
|
const [createError, setCreateError] = useState<string | null>(null);
|
||||||
|
const [creating, setCreating] = useState(false);
|
||||||
|
|
||||||
const projectsState = useProjects(state.interactiveData);
|
const projectsState = useProjects(state.interactiveData);
|
||||||
const selectedProject = projectsState.projects[projectIndex] ?? null;
|
const selectedProject = projectsState.projects[projectIndex] ?? null;
|
||||||
@@ -910,11 +957,29 @@ function BoardView({ state }: { state: DashboardState }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (subView === "create") {
|
||||||
|
// TextInput owns most key handling; only Esc cancels here. Submit is
|
||||||
|
// wired through the TextInput's onSubmit prop.
|
||||||
|
if (key.escape) {
|
||||||
|
setSubView("board");
|
||||||
|
setNewTaskTitle("");
|
||||||
|
setCreateError(null);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (input === "p" || input === "P") {
|
if (input === "p" || input === "P") {
|
||||||
setPickerOriginal(projectIndex);
|
setPickerOriginal(projectIndex);
|
||||||
setSubView("picker");
|
setSubView("picker");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (input === "n" || input === "N") {
|
||||||
|
if (!selectedProject) return;
|
||||||
|
setNewTaskTitle("");
|
||||||
|
setCreateError(null);
|
||||||
|
setSubView("create");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (key.return) {
|
if (key.return) {
|
||||||
if (selectedTask) setSubView("detail");
|
if (selectedTask) setSubView("detail");
|
||||||
return;
|
return;
|
||||||
@@ -947,7 +1012,33 @@ function BoardView({ state }: { state: DashboardState }) {
|
|||||||
? "↑↓ pick · Enter confirm · Esc cancel"
|
? "↑↓ pick · Enter confirm · Esc cancel"
|
||||||
: subView === "detail"
|
: subView === "detail"
|
||||||
? "Esc back · q quit"
|
? "Esc back · q quit"
|
||||||
: "←→ column · ↑↓ task · Enter open · p project";
|
: subView === "create"
|
||||||
|
? "type a task title · Enter create · Esc cancel"
|
||||||
|
: "←→ column · ↑↓ task · Enter open · n new · p project";
|
||||||
|
|
||||||
|
const submitNewTask = async () => {
|
||||||
|
const title = newTaskTitle.trim();
|
||||||
|
if (!title) {
|
||||||
|
setCreateError("Title cannot be empty");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!state.interactiveData || !selectedProject) {
|
||||||
|
setCreateError("No project selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCreating(true);
|
||||||
|
setCreateError(null);
|
||||||
|
try {
|
||||||
|
await state.interactiveData.createTask(selectedProject.path, { title });
|
||||||
|
setNewTaskTitle("");
|
||||||
|
setSubView("board");
|
||||||
|
tasksState.refresh();
|
||||||
|
} catch (err) {
|
||||||
|
setCreateError(err instanceof Error ? err.message : String(err));
|
||||||
|
} finally {
|
||||||
|
setCreating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" flexGrow={1}>
|
<Box flexDirection="column" flexGrow={1}>
|
||||||
@@ -964,7 +1055,44 @@ function BoardView({ state }: { state: DashboardState }) {
|
|||||||
|
|
||||||
<Box height={1} />
|
<Box height={1} />
|
||||||
|
|
||||||
{subView === "detail" && selectedTask ? (
|
{subView === "create" ? (
|
||||||
|
<Box justifyContent="center" alignItems="center" flexGrow={1}>
|
||||||
|
<Box
|
||||||
|
borderStyle="round"
|
||||||
|
borderColor="cyan"
|
||||||
|
flexDirection="column"
|
||||||
|
paddingX={2}
|
||||||
|
paddingY={1}
|
||||||
|
width={Math.min(80, Math.max(40, cols - 8))}
|
||||||
|
>
|
||||||
|
<Text bold color="cyanBright">New Task</Text>
|
||||||
|
<Text dimColor>Project: {selectedProject?.name ?? "(none)"}</Text>
|
||||||
|
<Box height={1} />
|
||||||
|
<Text dimColor>Title</Text>
|
||||||
|
<Box>
|
||||||
|
<Text color="cyanBright">▸ </Text>
|
||||||
|
<TextInput
|
||||||
|
value={newTaskTitle}
|
||||||
|
onChange={setNewTaskTitle}
|
||||||
|
onSubmit={() => void submitNewTask()}
|
||||||
|
placeholder="What needs doing?"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box height={1} />
|
||||||
|
{createError && (
|
||||||
|
<Text color="red">{createError}</Text>
|
||||||
|
)}
|
||||||
|
{creating ? (
|
||||||
|
<Box flexDirection="row" gap={1}>
|
||||||
|
<Text color="cyanBright"><Spinner type="dots" /></Text>
|
||||||
|
<Text dimColor>Creating…</Text>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Text dimColor>Enter to create · Esc to cancel</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
) : subView === "detail" && selectedTask ? (
|
||||||
<Box flexGrow={1} paddingX={1}>
|
<Box flexGrow={1} paddingX={1}>
|
||||||
<TaskDetailScreen task={selectedTask} />
|
<TaskDetailScreen task={selectedTask} />
|
||||||
</Box>
|
</Box>
|
||||||
@@ -1016,6 +1144,9 @@ function heartbeatFreshness(lastHeartbeatAt?: string): { fresh: boolean; label:
|
|||||||
type AgentSubView = "list" | "confirm-delete";
|
type AgentSubView = "list" | "confirm-delete";
|
||||||
|
|
||||||
function AgentsView({ state }: { state: DashboardState }) {
|
function AgentsView({ state }: { state: DashboardState }) {
|
||||||
|
const { stdout } = useStdout();
|
||||||
|
const cols = stdout?.columns ?? 80;
|
||||||
|
const isNarrow = cols < 80;
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
const [agents, setAgents] = useState<AgentItem[]>([]);
|
const [agents, setAgents] = useState<AgentItem[]>([]);
|
||||||
const [detail, setDetail] = useState<AgentDetailItem | null>(null);
|
const [detail, setDetail] = useState<AgentDetailItem | null>(null);
|
||||||
@@ -1146,13 +1277,15 @@ function AgentsView({ state }: { state: DashboardState }) {
|
|||||||
<Text color="yellow">{statusMsg}</Text>
|
<Text color="yellow">{statusMsg}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<Box flexDirection="row" flexGrow={1} overflow="hidden">
|
<Box flexDirection={isNarrow ? "column" : "row"} flexGrow={1} overflow="hidden">
|
||||||
{/* Left: agent list */}
|
{/* List panel */}
|
||||||
<Box
|
<Box
|
||||||
borderStyle="round"
|
borderStyle="round"
|
||||||
borderColor={detailFocused ? "gray" : "cyan"}
|
borderColor={detailFocused ? "gray" : "cyan"}
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
width="30%"
|
width={isNarrow ? undefined : "30%"}
|
||||||
|
flexGrow={isNarrow ? 1 : 0}
|
||||||
|
flexShrink={0}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
>
|
>
|
||||||
<Box paddingX={1}>
|
<Box paddingX={1}>
|
||||||
@@ -1270,7 +1403,7 @@ function AgentsView({ state }: { state: DashboardState }) {
|
|||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<Box paddingX={1}>
|
<Box paddingX={1}>
|
||||||
<Text dimColor>[s] start [x] stop [D] delete [r] refresh [Tab] switch panel focus ↑↓ select</Text>
|
<Text dimColor>[s] start [x] stop [D] delete [r] refresh [Tab] focus ↑↓ select</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -1622,6 +1755,15 @@ export function DashboardApp({ controller }: DashboardAppProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Status mode: Enter from the System panel opens the dashboard URL in
|
||||||
|
// the user's browser. Logs panel handles Enter itself (expand log entry)
|
||||||
|
// so we gate by activeSection.
|
||||||
|
if (key.return && state.activeSection === "system" && state.systemInfo) {
|
||||||
|
const url = state.systemInfo.tokenizedUrl ?? state.systemInfo.baseUrl;
|
||||||
|
openInBrowser(url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Number keys switch panels (status mode)
|
// Number keys switch panels (status mode)
|
||||||
if (input >= "1" && input <= "5") {
|
if (input >= "1" && input <= "5") {
|
||||||
const section = SECTION_ORDER[parseInt(input, 10) - 1];
|
const section = SECTION_ORDER[parseInt(input, 10) - 1];
|
||||||
@@ -1729,7 +1871,7 @@ export function DashboardApp({ controller }: DashboardAppProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isNarrow = cols < 100 || rows < 24;
|
const isNarrow = cols < 80 || rows < 20;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" height={rows}>
|
<Box flexDirection="column" height={rows}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import type { ProjectItem, TaskItem, InteractiveData } from "../state.js";
|
import type { ProjectItem, TaskItem, InteractiveData } from "../state.js";
|
||||||
|
|
||||||
export interface ProjectsState {
|
export interface ProjectsState {
|
||||||
@@ -11,44 +11,60 @@ export interface TasksState {
|
|||||||
tasks: TaskItem[];
|
tasks: TaskItem[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
refresh: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useProjects(interactiveData: InteractiveData | null): ProjectsState {
|
export function useProjects(interactiveData: InteractiveData | null): ProjectsState {
|
||||||
const [state, setState] = useState<ProjectsState>({ projects: [], loading: false, error: null });
|
const [projects, setProjects] = useState<ProjectItem[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!interactiveData) return;
|
if (!interactiveData) return;
|
||||||
setState({ projects: [], loading: true, error: null });
|
setLoading(true);
|
||||||
interactiveData.listProjects().then((projects) => {
|
setError(null);
|
||||||
setState({ projects, loading: false, error: null });
|
interactiveData.listProjects().then((p) => {
|
||||||
|
setProjects(p);
|
||||||
|
setLoading(false);
|
||||||
}).catch((err: unknown) => {
|
}).catch((err: unknown) => {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
setProjects([]);
|
||||||
setState({ projects: [], loading: false, error: message });
|
setLoading(false);
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
});
|
});
|
||||||
}, [interactiveData]);
|
}, [interactiveData]);
|
||||||
|
|
||||||
return state;
|
return { projects, loading, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTasks(
|
export function useTasks(
|
||||||
interactiveData: InteractiveData | null,
|
interactiveData: InteractiveData | null,
|
||||||
selectedProject: ProjectItem | null,
|
selectedProject: ProjectItem | null,
|
||||||
): TasksState {
|
): TasksState {
|
||||||
const [state, setState] = useState<TasksState>({ tasks: [], loading: false, error: null });
|
const [tasks, setTasks] = useState<TaskItem[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [reloadTick, setReloadTick] = useState(0);
|
||||||
|
|
||||||
|
const refresh = useCallback(() => setReloadTick((n) => n + 1), []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!interactiveData || !selectedProject) {
|
if (!interactiveData || !selectedProject) {
|
||||||
setState({ tasks: [], loading: false, error: null });
|
setTasks([]);
|
||||||
|
setLoading(false);
|
||||||
|
setError(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState({ tasks: [], loading: true, error: null });
|
setLoading(true);
|
||||||
interactiveData.listTasks(selectedProject.path).then((tasks) => {
|
setError(null);
|
||||||
setState({ tasks, loading: false, error: null });
|
interactiveData.listTasks(selectedProject.path).then((t) => {
|
||||||
|
setTasks(t);
|
||||||
|
setLoading(false);
|
||||||
}).catch((err: unknown) => {
|
}).catch((err: unknown) => {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
setTasks([]);
|
||||||
setState({ tasks: [], loading: false, error: message });
|
setLoading(false);
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
});
|
});
|
||||||
}, [interactiveData, selectedProject]);
|
}, [interactiveData, selectedProject, reloadTick]);
|
||||||
|
|
||||||
return state;
|
return { tasks, loading, error, refresh };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ export interface ModelItem {
|
|||||||
export interface InteractiveData {
|
export interface InteractiveData {
|
||||||
listProjects: () => Promise<ProjectItem[]>;
|
listProjects: () => Promise<ProjectItem[]>;
|
||||||
listTasks: (projectPath: string) => Promise<TaskItem[]>;
|
listTasks: (projectPath: string) => Promise<TaskItem[]>;
|
||||||
|
createTask: (projectPath: string, input: { title: string; description?: string }) => Promise<TaskItem>;
|
||||||
listAgents: () => Promise<AgentItem[]>;
|
listAgents: () => Promise<AgentItem[]>;
|
||||||
getAgentDetail: (id: string) => Promise<AgentDetailItem | null>;
|
getAgentDetail: (id: string) => Promise<AgentDetailItem | null>;
|
||||||
updateAgentState: (id: string, state: string) => Promise<void>;
|
updateAgentState: (id: string, state: string) => Promise<void>;
|
||||||
|
|||||||
@@ -1546,6 +1546,25 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
|||||||
agentState: (t as { agentState?: string }).agentState,
|
agentState: (t as { agentState?: string }).agentState,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
createTask: async (projectPath: string, input: { title: string; description?: string }) => {
|
||||||
|
let projectStore = projectStores.get(projectPath);
|
||||||
|
if (!projectStore) {
|
||||||
|
projectStore = projectPath === cwd ? store : new TaskStore(projectPath);
|
||||||
|
if (projectPath !== cwd) await projectStore.init();
|
||||||
|
projectStores.set(projectPath, projectStore);
|
||||||
|
}
|
||||||
|
const created = await projectStore.createTask({
|
||||||
|
title: input.title,
|
||||||
|
description: input.description ?? input.title,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
id: created.id,
|
||||||
|
title: created.title,
|
||||||
|
description: created.description ?? "",
|
||||||
|
column: created.column,
|
||||||
|
agentState: (created as { agentState?: string }).agentState,
|
||||||
|
};
|
||||||
|
},
|
||||||
listAgents: async () => {
|
listAgents: async () => {
|
||||||
const list = await agentStore!.listAgents();
|
const list = await agentStore!.listAgents();
|
||||||
return list.map((a) => ({
|
return list.map((a) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user