agent-nuvira 2.7.0 → 2.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/orchestrator.d.ts.map +1 -1
- package/dist/agents/orchestrator.js +17 -3
- package/dist/agents/orchestrator.js.map +1 -1
- package/dist/app.js +26 -0
- package/dist/cli/chat.d.ts.map +1 -1
- package/dist/cli/chat.js +108 -2
- package/dist/cli/chat.js.map +1 -1
- package/dist/config/auth.js +18 -0
- package/dist/config/jwt.js +24 -0
- package/dist/config/keys.js +14 -0
- package/dist/gateway/chat-store.d.ts +32 -5
- package/dist/gateway/chat-store.d.ts.map +1 -1
- package/dist/gateway/chat-store.js +52 -6
- package/dist/gateway/chat-store.js.map +1 -1
- package/dist/gateway/registry.d.ts.map +1 -1
- package/dist/gateway/registry.js +54 -10
- package/dist/gateway/registry.js.map +1 -1
- package/dist/inference/model-validator.d.ts.map +1 -1
- package/dist/inference/model-validator.js +11 -1
- package/dist/inference/model-validator.js.map +1 -1
- package/dist/inference/tool-call-utils.d.ts +31 -0
- package/dist/inference/tool-call-utils.d.ts.map +1 -1
- package/dist/inference/tool-call-utils.js +41 -0
- package/dist/inference/tool-call-utils.js.map +1 -1
- package/dist/learning/reasoning-trace.d.ts +5 -3
- package/dist/learning/reasoning-trace.d.ts.map +1 -1
- package/dist/learning/reasoning-trace.js +2 -2
- package/dist/learning/reasoning-trace.js.map +1 -1
- package/dist/middleware/auth.js +30 -0
- package/dist/nlu/conversation-gate.js +1 -1
- package/dist/nlu/conversation-gate.js.map +1 -1
- package/dist/nlu/entities.d.ts.map +1 -1
- package/dist/nlu/entities.js +12 -0
- package/dist/nlu/entities.js.map +1 -1
- package/dist/nlu/intent.d.ts.map +1 -1
- package/dist/nlu/intent.js +83 -15
- package/dist/nlu/intent.js.map +1 -1
- package/dist/passport.js +34 -0
- package/dist/routes/auth.js +42 -0
- package/dist/routes/user.js +31 -0
- package/dist/server.js +17 -0
- package/dist/tools/pipeline-tool.d.ts.map +1 -1
- package/dist/tools/pipeline-tool.js +14 -0
- package/dist/tools/pipeline-tool.js.map +1 -1
- package/dist/tools/release-sync.d.ts +52 -16
- package/dist/tools/release-sync.d.ts.map +1 -1
- package/dist/tools/release-sync.js +143 -79
- package/dist/tools/release-sync.js.map +1 -1
- package/dist/web-dashboard/src/App.js +85 -0
- package/dist/web-dashboard/src/admin-auth.test.js +186 -0
- package/dist/web-dashboard/src/ansi.js +23 -0
- package/dist/web-dashboard/src/ansi.test.js +31 -0
- package/dist/web-dashboard/src/api-admin-auth.test.js +172 -0
- package/dist/web-dashboard/src/api-admin.test.js +65 -0
- package/dist/web-dashboard/src/api-hub.test.js +117 -0
- package/dist/web-dashboard/src/api.js +1421 -0
- package/dist/web-dashboard/src/api.test.js +51 -0
- package/dist/web-dashboard/src/artifacts.js +128 -0
- package/dist/web-dashboard/src/artifacts.test.js +139 -0
- package/dist/web-dashboard/src/components/AdminPanel.js +567 -0
- package/dist/web-dashboard/src/components/AdminPanel.test.js +288 -0
- package/dist/web-dashboard/src/components/AgentHub.js +1580 -0
- package/dist/web-dashboard/src/components/AgentHub.test.js +343 -0
- package/dist/web-dashboard/src/components/BedrockOnboarding.js +320 -0
- package/dist/web-dashboard/src/components/BenchmarkCharts.js +228 -0
- package/dist/web-dashboard/src/components/ChatPage.js +1586 -0
- package/dist/web-dashboard/src/components/ChatPage.test.js +899 -0
- package/dist/web-dashboard/src/components/ContactsPage.js +141 -0
- package/dist/web-dashboard/src/components/CostDashboard.js +105 -0
- package/dist/web-dashboard/src/components/DAGView.js +477 -0
- package/dist/web-dashboard/src/components/DAGView.test.js +147 -0
- package/dist/web-dashboard/src/components/EnvVarEditor.js +138 -0
- package/dist/web-dashboard/src/components/EvalsPage.js +73 -0
- package/dist/web-dashboard/src/components/EvalsPage.test.js +120 -0
- package/dist/web-dashboard/src/components/ExecutionHistory.js +201 -0
- package/dist/web-dashboard/src/components/GatewayPage.js +40 -0
- package/dist/web-dashboard/src/components/GatewayPage.test.js +74 -0
- package/dist/web-dashboard/src/components/HealthPanel.js +68 -0
- package/dist/web-dashboard/src/components/HistoryBrowser.js +34 -0
- package/dist/web-dashboard/src/components/Layout.js +50 -0
- package/dist/web-dashboard/src/components/Markdown.js +152 -0
- package/dist/web-dashboard/src/components/Markdown.test.js +126 -0
- package/dist/web-dashboard/src/components/MarkdownZeroDep.js +237 -0
- package/dist/web-dashboard/src/components/MemoryPanel.js +81 -0
- package/dist/web-dashboard/src/components/ModelTimeline.js +298 -0
- package/dist/web-dashboard/src/components/ModelsPanel.js +1484 -0
- package/dist/web-dashboard/src/components/ModelsPanel.test.js +460 -0
- package/dist/web-dashboard/src/components/Overview.js +123 -0
- package/dist/web-dashboard/src/components/PhaseTimeline.js +359 -0
- package/dist/web-dashboard/src/components/PhaseTimeline.test.js +234 -0
- package/dist/web-dashboard/src/components/PlatformConfigSection.js +384 -0
- package/dist/web-dashboard/src/components/PlatformConfigSection.test.js +106 -0
- package/dist/web-dashboard/src/components/PlatformsPage.js +250 -0
- package/dist/web-dashboard/src/components/QuotaPanel.js +159 -0
- package/dist/web-dashboard/src/components/QuotaPanel.test.js +85 -0
- package/dist/web-dashboard/src/components/RequestsPanel.js +229 -0
- package/dist/web-dashboard/src/components/RequestsPanel.test.js +103 -0
- package/dist/web-dashboard/src/components/RoutingInsightsPanel.js +938 -0
- package/dist/web-dashboard/src/components/RoutingInsightsPanel.test.js +339 -0
- package/dist/web-dashboard/src/components/RoutingWalkthrough.js +408 -0
- package/dist/web-dashboard/src/components/RoutingWalkthrough.test.js +209 -0
- package/dist/web-dashboard/src/components/TaskConsole.js +119 -0
- package/dist/web-dashboard/src/components/TaskConsole.test.js +123 -0
- package/dist/web-dashboard/src/components/TasksPage.js +256 -0
- package/dist/web-dashboard/src/components/TasksPage.test.js +103 -0
- package/dist/web-dashboard/src/components/TracePanel.js +306 -0
- package/dist/web-dashboard/src/components/WhatsAppPanel.js +195 -0
- package/dist/web-dashboard/src/components/WhatsAppPanel.test.js +122 -0
- package/dist/web-dashboard/src/jsonOrNull.js +37 -0
- package/dist/web-dashboard/src/main.js +15 -0
- package/dist/web-dashboard/src/mask.js +11 -0
- package/dist/web-dashboard/vite.config.js +23 -0
- package/dist/web-dashboard/vitest.config.js +15 -0
- package/package.json +5 -3
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* TaskConsole — a reusable "run a CLI command" console for the dashboard.
|
|
4
|
+
*
|
|
5
|
+
* Renders preset buttons + a custom-command input, starts the command through
|
|
6
|
+
* the P1 task runner (the REAL CLI as an isolated child process), and streams
|
|
7
|
+
* the live output via SSE with cancel. Used by the Evals tab and the Gateway
|
|
8
|
+
* ops tab — one console, many surfaces.
|
|
9
|
+
*
|
|
10
|
+
* Running commands is a write action: the console is gated behind the admin
|
|
11
|
+
* session + routing.operate (admin or operator).
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.default = TaskConsole;
|
|
15
|
+
const react_1 = require("react");
|
|
16
|
+
const api_1 = require("../api");
|
|
17
|
+
const STATUS_LABEL = {
|
|
18
|
+
running: '⏳ running',
|
|
19
|
+
done: '✅ done',
|
|
20
|
+
failed: '❌ failed',
|
|
21
|
+
cancelled: '⏹ cancelled',
|
|
22
|
+
timeout: '⏰ timed out',
|
|
23
|
+
error: '💥 error',
|
|
24
|
+
};
|
|
25
|
+
function TaskConsole({ presets, customPlaceholder, timeoutMs = 300_000, hint }) {
|
|
26
|
+
const [auth, setAuth] = (0, react_1.useState)(null);
|
|
27
|
+
const [running, setRunning] = (0, react_1.useState)(false);
|
|
28
|
+
const [error, setError] = (0, react_1.useState)('');
|
|
29
|
+
const [custom, setCustom] = (0, react_1.useState)('');
|
|
30
|
+
const [active, setActive] = (0, react_1.useState)(null);
|
|
31
|
+
const [logs, setLogs] = (0, react_1.useState)([]);
|
|
32
|
+
const logEndRef = (0, react_1.useRef)(null);
|
|
33
|
+
const subRef = (0, react_1.useRef)(null);
|
|
34
|
+
(0, react_1.useEffect)(() => {
|
|
35
|
+
void api_1.dashboardAPI.fetchAdminAuthStatus().then((s) => {
|
|
36
|
+
setAuth(s ? { authenticated: s.authenticated, role: s.role } : { authenticated: false, role: null });
|
|
37
|
+
});
|
|
38
|
+
}, []);
|
|
39
|
+
(0, react_1.useEffect)(() => {
|
|
40
|
+
try {
|
|
41
|
+
logEndRef.current?.scrollIntoView?.({ block: 'end' });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
/* jsdom / non-DOM environments */
|
|
45
|
+
}
|
|
46
|
+
}, [logs]);
|
|
47
|
+
(0, react_1.useEffect)(() => {
|
|
48
|
+
return () => {
|
|
49
|
+
subRef.current?.();
|
|
50
|
+
};
|
|
51
|
+
}, []);
|
|
52
|
+
const canRun = auth?.authenticated === true && (auth.role === 'admin' || auth.role === 'operator');
|
|
53
|
+
const start = (0, react_1.useCallback)(async (args, presetTimeoutMs) => {
|
|
54
|
+
if (running)
|
|
55
|
+
return;
|
|
56
|
+
setRunning(true);
|
|
57
|
+
setError('');
|
|
58
|
+
setLogs([]);
|
|
59
|
+
const r = await api_1.dashboardAPI.startTask(args, presetTimeoutMs ?? timeoutMs);
|
|
60
|
+
if (!r.ok || !r.task) {
|
|
61
|
+
setError(r.error || 'Could not start the command.');
|
|
62
|
+
setRunning(false);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const task = r.task;
|
|
66
|
+
setActive(task);
|
|
67
|
+
const init = await api_1.dashboardAPI.getTask(task.id);
|
|
68
|
+
if (init?.task) {
|
|
69
|
+
setLogs(init.task.logs.map((l) => ({ stream: l.stream, text: l.text })));
|
|
70
|
+
}
|
|
71
|
+
subRef.current?.();
|
|
72
|
+
subRef.current = api_1.dashboardAPI.subscribeTask(task.id, {
|
|
73
|
+
onLog: (line) => setLogs((l) => [...l, { stream: line.stream, text: line.text }]),
|
|
74
|
+
onStatus: (status) => {
|
|
75
|
+
setActive((t) => (t ? { ...t, status } : t));
|
|
76
|
+
if (status !== 'running')
|
|
77
|
+
setRunning(false);
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}, [running, timeoutMs]);
|
|
81
|
+
const cancel = (0, react_1.useCallback)(async () => {
|
|
82
|
+
if (!active)
|
|
83
|
+
return;
|
|
84
|
+
await api_1.dashboardAPI.cancelTask(active.id);
|
|
85
|
+
}, [active]);
|
|
86
|
+
return (<div className="ops-console">
|
|
87
|
+
{!auth?.authenticated ? (<div className="admin-login-hint">Log in (admin or operator) to run commands.</div>) : !canRun ? (<div className="admin-login-hint">Your role can view but not run commands (requires admin or operator).</div>) : (<>
|
|
88
|
+
<div className="ops-presets">
|
|
89
|
+
{presets.map((p) => (<button key={p.label} className="admin-refresh-btn" type="button" onClick={() => void start(p.args, p.timeoutMs)} disabled={running}>
|
|
90
|
+
{p.label}
|
|
91
|
+
</button>))}
|
|
92
|
+
</div>
|
|
93
|
+
<div className="ops-custom-row">
|
|
94
|
+
<input type="text" value={custom} onChange={(e) => setCustom(e.target.value)} placeholder={customPlaceholder} disabled={running} maxLength={160}/>
|
|
95
|
+
<button className="admin-refresh-btn" type="button" onClick={() => void start(custom.trim().split(/\s+/))} disabled={running || !custom.trim()}>
|
|
96
|
+
Run
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
{hint ? <p className="admin-hint">{hint}</p> : null}
|
|
100
|
+
</>)}
|
|
101
|
+
|
|
102
|
+
{error ? <div className="admin-row-msg admin-row-msg-err">{error}</div> : null}
|
|
103
|
+
|
|
104
|
+
{active ? (<div className="ops-console-box">
|
|
105
|
+
<div className="ops-console-head">
|
|
106
|
+
<span>
|
|
107
|
+
{active.command} · <span className={active.status === 'running' ? 'task-status-running' : ''}>{STATUS_LABEL[active.status]}</span>
|
|
108
|
+
</span>
|
|
109
|
+
{active.status === 'running' ? (<button className="admin-refresh-btn" type="button" onClick={() => void cancel()}>
|
|
110
|
+
⏹ Cancel
|
|
111
|
+
</button>) : null}
|
|
112
|
+
</div>
|
|
113
|
+
<pre className="ops-console" role="log">
|
|
114
|
+
{logs.length > 0 ? logs.map((l, i) => <div key={i}>{l.text}</div>) : <div className="admin-hint">Waiting for output…</div>}
|
|
115
|
+
<div ref={logEndRef}/>
|
|
116
|
+
</pre>
|
|
117
|
+
</div>) : null}
|
|
118
|
+
</div>);
|
|
119
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* P2/P3 — TaskConsole tests (the shared CLI-run console).
|
|
4
|
+
*
|
|
5
|
+
* Used by the Evals tab and the Gateway ops tab: preset buttons + a custom
|
|
6
|
+
* command input start the REAL CLI through the mocked task API, and the live
|
|
7
|
+
* SSE console (subscribeTask) streams output with cancel. The console is the
|
|
8
|
+
* shared surface, so its mechanics are tested here once.
|
|
9
|
+
*/
|
|
10
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
11
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
12
|
+
};
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
const vitest_1 = require("vitest");
|
|
15
|
+
const react_1 = require("@testing-library/react");
|
|
16
|
+
const TaskConsole_1 = __importDefault(require("./TaskConsole"));
|
|
17
|
+
const api_1 = require("../api");
|
|
18
|
+
const PRESETS = [
|
|
19
|
+
{ label: '🌐 Gateway status', args: ['gateway', 'status'] },
|
|
20
|
+
{ label: '▶️ Start gateway (foreground)', args: ['gateway', 'start', '--no-events'] },
|
|
21
|
+
];
|
|
22
|
+
const TASK = {
|
|
23
|
+
id: 'task-1',
|
|
24
|
+
command: 'gateway status',
|
|
25
|
+
args: ['gateway', 'status'],
|
|
26
|
+
cwd: '/tmp',
|
|
27
|
+
status: 'running',
|
|
28
|
+
exitCode: null,
|
|
29
|
+
startedAt: Date.now(),
|
|
30
|
+
finishedAt: null,
|
|
31
|
+
durationMs: null,
|
|
32
|
+
timeoutMs: 300000,
|
|
33
|
+
logs: [],
|
|
34
|
+
};
|
|
35
|
+
function mockAuthed(authenticated = true, role = 'admin') {
|
|
36
|
+
vitest_1.vi.spyOn(api_1.dashboardAPI, 'fetchAdminAuthStatus').mockResolvedValue({
|
|
37
|
+
configured: true,
|
|
38
|
+
authenticated,
|
|
39
|
+
user: authenticated ? 'admin' : null,
|
|
40
|
+
role: authenticated ? role : null,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function mockRunApi() {
|
|
44
|
+
vitest_1.vi.spyOn(api_1.dashboardAPI, 'startTask').mockResolvedValue({ ok: true, task: TASK });
|
|
45
|
+
vitest_1.vi.spyOn(api_1.dashboardAPI, 'getTask').mockResolvedValue({ status: 200, task: TASK });
|
|
46
|
+
vitest_1.vi.spyOn(api_1.dashboardAPI, 'subscribeTask').mockReturnValue(() => { });
|
|
47
|
+
}
|
|
48
|
+
(0, vitest_1.afterEach)(() => {
|
|
49
|
+
(0, react_1.cleanup)();
|
|
50
|
+
vitest_1.vi.restoreAllMocks();
|
|
51
|
+
(0, api_1.setAdminToken)(null);
|
|
52
|
+
});
|
|
53
|
+
(0, vitest_1.describe)('TaskConsole', () => {
|
|
54
|
+
(0, vitest_1.it)('gates running behind admin/operator roles', async () => {
|
|
55
|
+
mockAuthed(false);
|
|
56
|
+
(0, react_1.render)(<TaskConsole_1.default presets={PRESETS} customPlaceholder="run something"/>);
|
|
57
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByText(/Log in \(admin or operator\)/)).toBeTruthy());
|
|
58
|
+
mockAuthed(true, 'viewer');
|
|
59
|
+
(0, react_1.cleanup)();
|
|
60
|
+
(0, react_1.render)(<TaskConsole_1.default presets={PRESETS} customPlaceholder="run something"/>);
|
|
61
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByText(/view but not run/)).toBeTruthy());
|
|
62
|
+
(0, vitest_1.expect)(react_1.screen.queryByRole('button', { name: /Gateway status/ })).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
(0, vitest_1.it)('a preset button starts its args with the timeout', async () => {
|
|
65
|
+
mockAuthed();
|
|
66
|
+
mockRunApi();
|
|
67
|
+
const start = vitest_1.vi.mocked(api_1.dashboardAPI.startTask);
|
|
68
|
+
(0, react_1.render)(<TaskConsole_1.default presets={PRESETS} customPlaceholder="run something" timeoutMs={123456}/>);
|
|
69
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByRole('button', { name: /Gateway status/ })).toBeTruthy());
|
|
70
|
+
react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /Gateway status/ }));
|
|
71
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(start).toHaveBeenCalledWith(['gateway', 'status'], 123456));
|
|
72
|
+
});
|
|
73
|
+
(0, vitest_1.it)('the custom input splits the command line into args', async () => {
|
|
74
|
+
mockAuthed();
|
|
75
|
+
mockRunApi();
|
|
76
|
+
const start = vitest_1.vi.mocked(api_1.dashboardAPI.startTask);
|
|
77
|
+
(0, react_1.render)(<TaskConsole_1.default presets={PRESETS} customPlaceholder="run something"/>);
|
|
78
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByPlaceholderText('run something')).toBeTruthy());
|
|
79
|
+
const input = react_1.screen.getByPlaceholderText('run something');
|
|
80
|
+
react_1.fireEvent.change(input, { target: { value: 'admin cron run nightly' } });
|
|
81
|
+
react_1.fireEvent.click(react_1.screen.getByRole('button', { name: 'Run' }));
|
|
82
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(start).toHaveBeenCalledWith(['admin', 'cron', 'run', 'nightly'], 300_000));
|
|
83
|
+
});
|
|
84
|
+
(0, vitest_1.it)('streams the live console, offers cancel, and stops on completion', async () => {
|
|
85
|
+
mockAuthed();
|
|
86
|
+
mockRunApi();
|
|
87
|
+
let onLog = null;
|
|
88
|
+
let onStatus = null;
|
|
89
|
+
vitest_1.vi.spyOn(api_1.dashboardAPI, 'subscribeTask').mockImplementation((_id, handlers) => {
|
|
90
|
+
onLog = handlers.onLog;
|
|
91
|
+
onStatus = handlers.onStatus;
|
|
92
|
+
return vitest_1.vi.fn();
|
|
93
|
+
});
|
|
94
|
+
vitest_1.vi.spyOn(api_1.dashboardAPI, 'getTask').mockResolvedValue({
|
|
95
|
+
status: 200,
|
|
96
|
+
task: { ...TASK, logs: [{ stream: 'stdout', text: 'gateway: channels loaded', at: Date.now() }] },
|
|
97
|
+
});
|
|
98
|
+
const cancel = vitest_1.vi.spyOn(api_1.dashboardAPI, 'cancelTask').mockResolvedValue({ ok: true });
|
|
99
|
+
(0, react_1.render)(<TaskConsole_1.default presets={PRESETS} customPlaceholder="run something"/>);
|
|
100
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByRole('button', { name: /Gateway status/ })).toBeTruthy());
|
|
101
|
+
react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /Gateway status/ }));
|
|
102
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByText('gateway: channels loaded')).toBeTruthy());
|
|
103
|
+
// Live line streams in while running.
|
|
104
|
+
onLog({ stream: 'stdout', text: 'telegram: connected' });
|
|
105
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByText('telegram: connected')).toBeTruthy());
|
|
106
|
+
(0, vitest_1.expect)(react_1.screen.getByRole('button', { name: /Cancel/ })).toBeTruthy();
|
|
107
|
+
react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /Cancel/ }));
|
|
108
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(cancel).toHaveBeenCalledWith('task-1'));
|
|
109
|
+
// Completion turns the console to done and drops the cancel button.
|
|
110
|
+
onStatus('done');
|
|
111
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByText(/done/)).toBeTruthy());
|
|
112
|
+
(0, vitest_1.expect)(react_1.screen.queryByRole('button', { name: /Cancel/ })).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
(0, vitest_1.it)('surfaces a start failure instead of streaming', async () => {
|
|
115
|
+
mockAuthed();
|
|
116
|
+
vitest_1.vi.spyOn(api_1.dashboardAPI, 'startTask').mockResolvedValue({ ok: false, error: 'CLI not found' });
|
|
117
|
+
(0, react_1.render)(<TaskConsole_1.default presets={PRESETS} customPlaceholder="run something"/>);
|
|
118
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByRole('button', { name: /Gateway status/ })).toBeTruthy());
|
|
119
|
+
react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /Gateway status/ }));
|
|
120
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByText(/CLI not found/)).toBeTruthy());
|
|
121
|
+
(0, vitest_1.expect)(react_1.screen.queryByRole('button', { name: /Cancel/ })).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* TasksPage — P1 command console.
|
|
4
|
+
*
|
|
5
|
+
* The dashboard's task runner executes the agent-nuvira CLI as an isolated
|
|
6
|
+
* child process (`node dist/index.js <args>`), so every command that works in
|
|
7
|
+
* a terminal works here too — the GUI is literally running the CLI. This page
|
|
8
|
+
* provides the run form, a live log console (SSE), cancel, and history.
|
|
9
|
+
*
|
|
10
|
+
* Running commands is a write action: the page is gated behind the same admin
|
|
11
|
+
* session as the other action surfaces (Admin, Agent Hub toggles).
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.default = TasksPage;
|
|
15
|
+
const react_1 = require("react");
|
|
16
|
+
const react_router_dom_1 = require("react-router-dom");
|
|
17
|
+
const api_1 = require("../api");
|
|
18
|
+
const STATUS_LABEL = {
|
|
19
|
+
running: '⏳ running',
|
|
20
|
+
done: '✅ done',
|
|
21
|
+
failed: '❌ failed',
|
|
22
|
+
cancelled: '⏹ cancelled',
|
|
23
|
+
timeout: '⏰ timed out',
|
|
24
|
+
error: '💥 error',
|
|
25
|
+
};
|
|
26
|
+
function statusClass(status) {
|
|
27
|
+
switch (status) {
|
|
28
|
+
case 'done': return 'task-status-done';
|
|
29
|
+
case 'running': return 'task-status-running';
|
|
30
|
+
case 'failed': return 'task-status-failed';
|
|
31
|
+
case 'cancelled': return 'task-status-cancelled';
|
|
32
|
+
case 'timeout': return 'task-status-timeout';
|
|
33
|
+
default: return 'task-status-error';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Split a command line into argv (whitespace, quote-aware). */
|
|
37
|
+
function splitArgs(line) {
|
|
38
|
+
const out = [];
|
|
39
|
+
let cur = '';
|
|
40
|
+
let quote = null;
|
|
41
|
+
for (const ch of line.trim()) {
|
|
42
|
+
if (quote) {
|
|
43
|
+
if (ch === quote)
|
|
44
|
+
quote = null;
|
|
45
|
+
else
|
|
46
|
+
cur += ch;
|
|
47
|
+
}
|
|
48
|
+
else if (ch === '"' || ch === "'") {
|
|
49
|
+
quote = ch;
|
|
50
|
+
}
|
|
51
|
+
else if (/\s/.test(ch)) {
|
|
52
|
+
if (cur) {
|
|
53
|
+
out.push(cur);
|
|
54
|
+
cur = '';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
cur += ch;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (cur)
|
|
62
|
+
out.push(cur);
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
function fmtDuration(ms) {
|
|
66
|
+
if (ms === null)
|
|
67
|
+
return '—';
|
|
68
|
+
if (ms < 1000)
|
|
69
|
+
return `${ms}ms`;
|
|
70
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
71
|
+
}
|
|
72
|
+
function fmtAge(at) {
|
|
73
|
+
const s = Math.max(0, Math.floor((Date.now() - at) / 1000));
|
|
74
|
+
if (s < 60)
|
|
75
|
+
return `${s}s ago`;
|
|
76
|
+
const m = Math.floor(s / 60);
|
|
77
|
+
if (m < 60)
|
|
78
|
+
return `${m}m ago`;
|
|
79
|
+
return `${Math.floor(m / 60)}h ago`;
|
|
80
|
+
}
|
|
81
|
+
function TasksPage() {
|
|
82
|
+
const [auth, setAuth] = (0, react_1.useState)(null);
|
|
83
|
+
const [command, setCommand] = (0, react_1.useState)('');
|
|
84
|
+
const [timeoutSec, setTimeoutSec] = (0, react_1.useState)(300);
|
|
85
|
+
const [starting, setStarting] = (0, react_1.useState)(false);
|
|
86
|
+
const [error, setError] = (0, react_1.useState)('');
|
|
87
|
+
// The task being displayed in the console (running or a selected history task).
|
|
88
|
+
const [active, setActive] = (0, react_1.useState)(null);
|
|
89
|
+
const [history, setHistory] = (0, react_1.useState)([]);
|
|
90
|
+
const [autoScroll, setAutoScroll] = (0, react_1.useState)(true);
|
|
91
|
+
const logEndRef = (0, react_1.useRef)(null);
|
|
92
|
+
const subRef = (0, react_1.useRef)(null);
|
|
93
|
+
const refreshHistory = (0, react_1.useCallback)(async () => {
|
|
94
|
+
const r = await api_1.dashboardAPI.listTasks();
|
|
95
|
+
if (r)
|
|
96
|
+
setHistory(r.tasks);
|
|
97
|
+
}, []);
|
|
98
|
+
(0, react_1.useEffect)(() => {
|
|
99
|
+
void api_1.dashboardAPI.fetchAdminAuthStatus().then((s) => {
|
|
100
|
+
setAuth(s
|
|
101
|
+
? { configured: s.configured, authenticated: s.authenticated, role: s.role }
|
|
102
|
+
: { configured: false, authenticated: false, role: null });
|
|
103
|
+
if (s?.authenticated)
|
|
104
|
+
void refreshHistory();
|
|
105
|
+
});
|
|
106
|
+
}, [refreshHistory]);
|
|
107
|
+
// Auto-scroll the console to the newest line while running.
|
|
108
|
+
(0, react_1.useEffect)(() => {
|
|
109
|
+
if (autoScroll && active?.status === 'running') {
|
|
110
|
+
try {
|
|
111
|
+
logEndRef.current?.scrollIntoView?.({ block: 'end' });
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
/* jsdom / non-DOM scroll environments */
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}, [active?.logs.length, active?.status, autoScroll]);
|
|
118
|
+
(0, react_1.useEffect)(() => () => subRef.current?.(), []);
|
|
119
|
+
const openTask = (0, react_1.useCallback)((task, subscribe) => {
|
|
120
|
+
subRef.current?.();
|
|
121
|
+
setActive(task);
|
|
122
|
+
setError('');
|
|
123
|
+
if (!subscribe)
|
|
124
|
+
return;
|
|
125
|
+
const off = api_1.dashboardAPI.subscribeTask(task.id, {
|
|
126
|
+
onLog: (line) => setActive((prev) => (prev && prev.id === task.id ? { ...prev, logs: [...prev.logs, line] } : prev)),
|
|
127
|
+
onStatus: (status) => setActive((prev) => (prev && prev.id === task.id ? { ...prev, status } : prev)),
|
|
128
|
+
});
|
|
129
|
+
subRef.current = off;
|
|
130
|
+
}, []);
|
|
131
|
+
const run = (0, react_1.useCallback)(async () => {
|
|
132
|
+
const args = splitArgs(command);
|
|
133
|
+
if (args.length === 0) {
|
|
134
|
+
setError('Type a command first — e.g. `eval run --task smoke-test` or `gateway status`.');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
setStarting(true);
|
|
138
|
+
setError('');
|
|
139
|
+
const r = await api_1.dashboardAPI.startTask(args, timeoutSec * 1000);
|
|
140
|
+
setStarting(false);
|
|
141
|
+
if (!r.ok || !r.task) {
|
|
142
|
+
setError(r.error || 'Failed to start task.');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
setCommand('');
|
|
146
|
+
void refreshHistory();
|
|
147
|
+
openTask(r.task, true);
|
|
148
|
+
}, [command, timeoutSec, openTask, refreshHistory]);
|
|
149
|
+
const cancel = (0, react_1.useCallback)(async (id) => {
|
|
150
|
+
await api_1.dashboardAPI.cancelTask(id);
|
|
151
|
+
}, []);
|
|
152
|
+
const showHistory = (0, react_1.useCallback)(async (id) => {
|
|
153
|
+
const r = await api_1.dashboardAPI.getTask(id);
|
|
154
|
+
if (r?.task)
|
|
155
|
+
openTask(r.task, false);
|
|
156
|
+
}, [openTask]);
|
|
157
|
+
const clearConsole = (0, react_1.useCallback)(() => {
|
|
158
|
+
subRef.current?.();
|
|
159
|
+
setActive(null);
|
|
160
|
+
}, []);
|
|
161
|
+
if (!auth) {
|
|
162
|
+
return (<div className="panel">
|
|
163
|
+
<h2 className="panel-title">🚀 Command Console</h2>
|
|
164
|
+
<div className="loading-state">Loading…</div>
|
|
165
|
+
</div>);
|
|
166
|
+
}
|
|
167
|
+
if (!auth.authenticated) {
|
|
168
|
+
return (<div className="panel">
|
|
169
|
+
<h2 className="panel-title">🚀 Command Console</h2>
|
|
170
|
+
<div className="empty-state">
|
|
171
|
+
<p>
|
|
172
|
+
<strong>Log in to run tasks.</strong> The console executes commands on this machine, so it needs an
|
|
173
|
+
admin session (like the Agent Hub toggles).
|
|
174
|
+
</p>
|
|
175
|
+
<p>
|
|
176
|
+
{auth.configured ? (<react_router_dom_1.Link className="admin-refresh-btn" to="/admin">🔐 Log in</react_router_dom_1.Link>) : (<react_router_dom_1.Link className="admin-refresh-btn" to="/admin">🔐 Create admin account</react_router_dom_1.Link>)}
|
|
177
|
+
</p>
|
|
178
|
+
</div>
|
|
179
|
+
</div>);
|
|
180
|
+
}
|
|
181
|
+
return (<div className="panel">
|
|
182
|
+
<h2 className="panel-title">🚀 Command Console</h2>
|
|
183
|
+
<p className="admin-subtitle">
|
|
184
|
+
Run any agent-nuvira command — the same CLI, executed by the dashboard. Messaging, skills, tools, eval,
|
|
185
|
+
memory, traces and more.
|
|
186
|
+
</p>
|
|
187
|
+
|
|
188
|
+
<form className="task-run-form" onSubmit={(e) => {
|
|
189
|
+
e.preventDefault();
|
|
190
|
+
void run();
|
|
191
|
+
}}>
|
|
192
|
+
<input className="admin-input task-cmd-input" placeholder="eval run --task smoke-test · gateway status · skill list · memory stats" value={command} onChange={(e) => setCommand(e.target.value)} autoComplete="off" spellCheck={false}/>
|
|
193
|
+
<select className="admin-input task-timeout-select" title="Task timeout" value={timeoutSec} onChange={(e) => setTimeoutSec(Number(e.target.value))}>
|
|
194
|
+
<option value={60}>1 min</option>
|
|
195
|
+
<option value={300}>5 min</option>
|
|
196
|
+
<option value={600}>10 min</option>
|
|
197
|
+
<option value={1800}>30 min</option>
|
|
198
|
+
</select>
|
|
199
|
+
<button className="admin-refresh-btn" type="submit" disabled={starting || command.trim() === ''}>
|
|
200
|
+
{starting ? '⏳ Starting…' : '▶ Run'}
|
|
201
|
+
</button>
|
|
202
|
+
</form>
|
|
203
|
+
{error && <div className="admin-error">{error}</div>}
|
|
204
|
+
<p className="task-hint">
|
|
205
|
+
Long-running commands stream their output live. Runs are capped at the chosen timeout and can be cancelled.
|
|
206
|
+
</p>
|
|
207
|
+
|
|
208
|
+
{active && (<div className="task-console-wrap">
|
|
209
|
+
<div className="task-console-header">
|
|
210
|
+
<code className="task-console-cmd">{active.command}</code>
|
|
211
|
+
<span className={`task-status ${statusClass(active.status)}`}>{STATUS_LABEL[active.status]}</span>
|
|
212
|
+
<span className="task-meta">
|
|
213
|
+
exit {active.exitCode ?? '—'} · {fmtDuration(active.durationMs)}
|
|
214
|
+
</span>
|
|
215
|
+
{active.status === 'running' && (<button className="admin-mini-btn task-cancel-btn" onClick={() => void cancel(active.id)}>
|
|
216
|
+
⏹ Cancel
|
|
217
|
+
</button>)}
|
|
218
|
+
<button className="admin-mini-btn" onClick={clearConsole}>✕ Close</button>
|
|
219
|
+
</div>
|
|
220
|
+
<div className="task-console">
|
|
221
|
+
{active.logs.map((line, i) => (<div key={i} className={`task-line task-line-${line.stream}`}>
|
|
222
|
+
{line.text}
|
|
223
|
+
</div>))}
|
|
224
|
+
{active.logs.length === 0 && active.status === 'running' && (<div className="task-line task-line-system">⏳ waiting for output…</div>)}
|
|
225
|
+
<div ref={logEndRef}/>
|
|
226
|
+
</div>
|
|
227
|
+
<label className="task-autoscroll">
|
|
228
|
+
<input type="checkbox" checked={autoScroll} onChange={(e) => setAutoScroll(e.target.checked)}/> auto-scroll
|
|
229
|
+
</label>
|
|
230
|
+
</div>)}
|
|
231
|
+
|
|
232
|
+
<h3 className="task-history-title">History</h3>
|
|
233
|
+
{history.length === 0 ? (<div className="empty-state">No tasks yet — run your first command above.</div>) : (<div className="admin-table-wrapper">
|
|
234
|
+
<table className="admin-table task-table">
|
|
235
|
+
<thead>
|
|
236
|
+
<tr>
|
|
237
|
+
<th>Command</th>
|
|
238
|
+
<th>Status</th>
|
|
239
|
+
<th>Exit</th>
|
|
240
|
+
<th>Duration</th>
|
|
241
|
+
<th>When</th>
|
|
242
|
+
</tr>
|
|
243
|
+
</thead>
|
|
244
|
+
<tbody>
|
|
245
|
+
{history.map((t) => (<tr key={t.id} className={active?.id === t.id ? 'task-row-active' : ''} onClick={() => void showHistory(t.id)}>
|
|
246
|
+
<td><code>{t.command}</code></td>
|
|
247
|
+
<td><span className={`task-status ${statusClass(t.status)}`}>{STATUS_LABEL[t.status]}</span></td>
|
|
248
|
+
<td>{t.exitCode ?? '—'}</td>
|
|
249
|
+
<td>{fmtDuration(t.durationMs)}</td>
|
|
250
|
+
<td>{fmtAge(t.startedAt)}</td>
|
|
251
|
+
</tr>))}
|
|
252
|
+
</tbody>
|
|
253
|
+
</table>
|
|
254
|
+
</div>)}
|
|
255
|
+
</div>);
|
|
256
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* TasksPage — command console tests (P1).
|
|
4
|
+
*
|
|
5
|
+
* - Unauthenticated users see the login gate (running commands is a write
|
|
6
|
+
* action, like the Agent Hub toggles).
|
|
7
|
+
* - Authenticated users get the run form + history table.
|
|
8
|
+
* - Typing a command and hitting Run calls startTask with the parsed argv and
|
|
9
|
+
* subscribes to the task's SSE stream.
|
|
10
|
+
* - The Run button is disabled while the command is empty / while starting.
|
|
11
|
+
*/
|
|
12
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
14
|
+
};
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
const vitest_1 = require("vitest");
|
|
17
|
+
const react_1 = require("@testing-library/react");
|
|
18
|
+
const react_router_dom_1 = require("react-router-dom");
|
|
19
|
+
const TasksPage_1 = __importDefault(require("./TasksPage"));
|
|
20
|
+
const api_1 = require("../api");
|
|
21
|
+
const TASK = {
|
|
22
|
+
id: 't1',
|
|
23
|
+
command: 'eval run --task smoke',
|
|
24
|
+
args: ['eval', 'run', '--task', 'smoke'],
|
|
25
|
+
cwd: '/workspace',
|
|
26
|
+
status: 'done',
|
|
27
|
+
exitCode: 0,
|
|
28
|
+
startedAt: Date.now() - 5000,
|
|
29
|
+
finishedAt: Date.now(),
|
|
30
|
+
durationMs: 5000,
|
|
31
|
+
timeoutMs: 300000,
|
|
32
|
+
logs: [{ stream: 'stdout', text: 'hello from fixture', at: Date.now() }],
|
|
33
|
+
};
|
|
34
|
+
function mockAuthed() {
|
|
35
|
+
vitest_1.vi.spyOn(api_1.dashboardAPI, 'fetchAdminAuthStatus').mockResolvedValue({
|
|
36
|
+
configured: true,
|
|
37
|
+
authenticated: true,
|
|
38
|
+
user: 'admin',
|
|
39
|
+
role: 'admin',
|
|
40
|
+
});
|
|
41
|
+
vitest_1.vi.spyOn(api_1.dashboardAPI, 'listTasks').mockResolvedValue({ status: 200, tasks: [TASK] });
|
|
42
|
+
}
|
|
43
|
+
(0, vitest_1.afterEach)(() => {
|
|
44
|
+
(0, react_1.cleanup)();
|
|
45
|
+
vitest_1.vi.restoreAllMocks();
|
|
46
|
+
(0, api_1.setAdminToken)(null);
|
|
47
|
+
});
|
|
48
|
+
function renderPage() {
|
|
49
|
+
(0, react_1.render)(<react_router_dom_1.MemoryRouter>
|
|
50
|
+
<TasksPage_1.default />
|
|
51
|
+
</react_router_dom_1.MemoryRouter>);
|
|
52
|
+
}
|
|
53
|
+
(0, vitest_1.describe)('TasksPage', () => {
|
|
54
|
+
(0, vitest_1.it)('shows the login gate when unauthenticated', async () => {
|
|
55
|
+
vitest_1.vi.spyOn(api_1.dashboardAPI, 'fetchAdminAuthStatus').mockResolvedValue({
|
|
56
|
+
configured: true,
|
|
57
|
+
authenticated: false,
|
|
58
|
+
user: null,
|
|
59
|
+
role: null,
|
|
60
|
+
});
|
|
61
|
+
renderPage();
|
|
62
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByText(/Log in to run tasks/)).toBeTruthy());
|
|
63
|
+
});
|
|
64
|
+
(0, vitest_1.it)('renders the run form and history when authed', async () => {
|
|
65
|
+
mockAuthed();
|
|
66
|
+
renderPage();
|
|
67
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByPlaceholderText(/eval run --task smoke-test/)).toBeTruthy());
|
|
68
|
+
// History table row shows the finished task.
|
|
69
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByText('eval run --task smoke')).toBeTruthy());
|
|
70
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/✅ done/)).toBeTruthy();
|
|
71
|
+
});
|
|
72
|
+
(0, vitest_1.it)('disables Run while the command is empty', async () => {
|
|
73
|
+
mockAuthed();
|
|
74
|
+
renderPage();
|
|
75
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByPlaceholderText(/eval run --task smoke-test/)).toBeTruthy());
|
|
76
|
+
(0, vitest_1.expect)(react_1.screen.getByRole('button', { name: /▶ Run/ }).disabled).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
(0, vitest_1.it)('starts a task from the typed command and subscribes to its stream', async () => {
|
|
79
|
+
mockAuthed();
|
|
80
|
+
const startMock = vitest_1.vi.spyOn(api_1.dashboardAPI, 'startTask').mockResolvedValue({
|
|
81
|
+
ok: true,
|
|
82
|
+
task: { ...TASK, id: 't2', command: 'memory stats', args: ['memory', 'stats'], status: 'running', logs: [] },
|
|
83
|
+
});
|
|
84
|
+
const subMock = vitest_1.vi.spyOn(api_1.dashboardAPI, 'subscribeTask').mockReturnValue(() => { });
|
|
85
|
+
renderPage();
|
|
86
|
+
const input = await react_1.screen.findByPlaceholderText(/eval run --task smoke-test/);
|
|
87
|
+
react_1.fireEvent.change(input, { target: { value: 'memory stats' } });
|
|
88
|
+
react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /▶ Run/ }));
|
|
89
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(startMock).toHaveBeenCalledWith(['memory', 'stats'], 300000));
|
|
90
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(subMock).toHaveBeenCalledWith('t2', vitest_1.expect.any(Object)));
|
|
91
|
+
// The console header shows the running command.
|
|
92
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByText('memory stats')).toBeTruthy());
|
|
93
|
+
});
|
|
94
|
+
(0, vitest_1.it)('shows a friendly error when start fails', async () => {
|
|
95
|
+
mockAuthed();
|
|
96
|
+
vitest_1.vi.spyOn(api_1.dashboardAPI, 'startTask').mockResolvedValue({ ok: false, error: 'Missing command args.' });
|
|
97
|
+
renderPage();
|
|
98
|
+
const input = await react_1.screen.findByPlaceholderText(/eval run --task smoke-test/);
|
|
99
|
+
react_1.fireEvent.change(input, { target: { value: 'eval run' } });
|
|
100
|
+
react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /▶ Run/ }));
|
|
101
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByText(/Missing command args/)).toBeTruthy());
|
|
102
|
+
});
|
|
103
|
+
});
|