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,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = HealthPanel;
|
|
4
|
+
function formatNumber(n) {
|
|
5
|
+
if (n === undefined || n === null)
|
|
6
|
+
return '0';
|
|
7
|
+
if (n >= 1_000_000)
|
|
8
|
+
return (n / 1_000_000).toFixed(1) + 'M';
|
|
9
|
+
if (n >= 1_000)
|
|
10
|
+
return (n / 1_000).toFixed(1) + 'K';
|
|
11
|
+
return n.toLocaleString();
|
|
12
|
+
}
|
|
13
|
+
function HealthPanel({ data }) {
|
|
14
|
+
if (!data) {
|
|
15
|
+
return <div className="loading-state"><p>Loading system data...</p></div>;
|
|
16
|
+
}
|
|
17
|
+
const { health, serverTime } = data;
|
|
18
|
+
return (<>
|
|
19
|
+
<h2 className="section-title">⚙️ System Health</h2>
|
|
20
|
+
|
|
21
|
+
<div className="health-grid">
|
|
22
|
+
<div className="health-card">
|
|
23
|
+
<span className="health-icon">💾</span>
|
|
24
|
+
<div className="health-body">
|
|
25
|
+
<div className="health-title">Memory Directory</div>
|
|
26
|
+
<div className="health-path">{health.memoryDir || '~/.buff/memory/'}</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<div className="health-card">
|
|
30
|
+
<span className="health-icon">📝</span>
|
|
31
|
+
<div className="health-body">
|
|
32
|
+
<div className="health-title">Coding Patterns</div>
|
|
33
|
+
<div className="health-value">{formatNumber(health.patterns)}</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
<div className="health-card">
|
|
37
|
+
<span className="health-icon">👍</span>
|
|
38
|
+
<div className="health-body">
|
|
39
|
+
<div className="health-title">User Feedback</div>
|
|
40
|
+
<div className="health-value">{formatNumber(health.feedback)} ratings</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<div className="health-card">
|
|
44
|
+
<span className="health-icon">🧠</span>
|
|
45
|
+
<div className="health-body">
|
|
46
|
+
<div className="health-title">Vector Index</div>
|
|
47
|
+
<div className="health-value">{formatNumber(health.vectors)} entries</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<h3 className="section-subtitle">Server Info</h3>
|
|
53
|
+
<div className="server-info">
|
|
54
|
+
<div className="info-row">
|
|
55
|
+
<span className="info-label">Status</span>
|
|
56
|
+
<span className="info-value">● Connected</span>
|
|
57
|
+
</div>
|
|
58
|
+
<div className="info-row">
|
|
59
|
+
<span className="info-label">Last Updated</span>
|
|
60
|
+
<span className="info-value">{serverTime ? new Date(serverTime).toLocaleString() : '--'}</span>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="info-row">
|
|
63
|
+
<span className="info-label">Data Directory</span>
|
|
64
|
+
<span className="info-value">{health.memoryDir || '~/.buff/memory/'}</span>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</>);
|
|
68
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = HistoryBrowser;
|
|
4
|
+
function formatTime(ts) {
|
|
5
|
+
const d = new Date(ts);
|
|
6
|
+
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
7
|
+
}
|
|
8
|
+
function HistoryBrowser({ data }) {
|
|
9
|
+
if (!data) {
|
|
10
|
+
return <div className="loading-state"><p>Loading history...</p></div>;
|
|
11
|
+
}
|
|
12
|
+
const { history } = data;
|
|
13
|
+
const sessions = history.recent || [];
|
|
14
|
+
return (<>
|
|
15
|
+
<h2 className="section-title">📝 Conversation History</h2>
|
|
16
|
+
<div className="history-stats">
|
|
17
|
+
<span className="history-count">Total: <strong>{history.total}</strong> sessions</span>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div className="history-list">
|
|
21
|
+
{sessions.length === 0 ? (<div className="empty-state">No conversations recorded yet.</div>) : (sessions.map((session) => (<div className="history-item" key={session.id}>
|
|
22
|
+
<div className="history-summary">{session.summary || 'Untitled'}</div>
|
|
23
|
+
<div className="history-meta">
|
|
24
|
+
<span>📅 {formatTime(session.startedAt)}</span>
|
|
25
|
+
<span>🤖 {session.provider || '--'}</span>
|
|
26
|
+
<span>💬 {session.messageCount || 0} msgs</span>
|
|
27
|
+
{session.tags && session.tags.length > 0 && (<span>
|
|
28
|
+
🏷️ {session.tags.map((tag) => (<span className="history-tag" key={tag}>{tag}</span>))}
|
|
29
|
+
</span>)}
|
|
30
|
+
</div>
|
|
31
|
+
</div>)))}
|
|
32
|
+
</div>
|
|
33
|
+
</>);
|
|
34
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = Layout;
|
|
4
|
+
const react_router_dom_1 = require("react-router-dom");
|
|
5
|
+
const NAV_ITEMS = [
|
|
6
|
+
// Phase 5 — Chat is the front door (the lobby); the other rooms are panels.
|
|
7
|
+
{ path: '/', label: 'Chat', icon: '💬' },
|
|
8
|
+
{ path: '/overview', label: 'Overview', icon: '📊' },
|
|
9
|
+
{ path: '/dag', label: 'Execution', icon: '🔀' },
|
|
10
|
+
{ path: '/tasks', label: 'Tasks', icon: '🚀' },
|
|
11
|
+
{ path: '/evals', label: 'Evals', icon: '🏆' },
|
|
12
|
+
{ path: '/platforms', label: 'Platforms', icon: '🌐' },
|
|
13
|
+
{ path: '/contacts', label: 'Contacts', icon: '📇' },
|
|
14
|
+
{ path: '/gateway', label: 'Gateway', icon: '📡' },
|
|
15
|
+
{ path: '/models', label: 'Models', icon: '🧠' },
|
|
16
|
+
{ path: '/models/timeline', label: 'Timeline', icon: '📅' },
|
|
17
|
+
{ path: '/routing', label: 'Routing', icon: '🤖' },
|
|
18
|
+
{ path: '/requests', label: 'Requests', icon: '📨' },
|
|
19
|
+
{ path: '/hub', label: 'Agent Hub', icon: '🧰' },
|
|
20
|
+
{ path: '/traces', label: 'Traces', icon: '🔍' },
|
|
21
|
+
{ path: '/costs', label: 'Costs', icon: '💰' },
|
|
22
|
+
{ path: '/benchmarks', label: 'Benchmarks', icon: '📈' },
|
|
23
|
+
{ path: '/system', label: 'System', icon: '⚙️' },
|
|
24
|
+
{ path: '/admin', label: 'Admin', icon: '🛠️' },
|
|
25
|
+
];
|
|
26
|
+
function Layout({ children, connected, lastUpdated }) {
|
|
27
|
+
const statusClass = connected ? 'connected' : 'reconnecting';
|
|
28
|
+
const statusText = connected ? 'Connected' : 'Reconnecting...';
|
|
29
|
+
return (<div className="layout">
|
|
30
|
+
<nav className="nav">
|
|
31
|
+
<div className="nav-header">
|
|
32
|
+
<span className="nav-logo">🤖</span>
|
|
33
|
+
<span className="nav-title">Agent-Nuvira</span>
|
|
34
|
+
</div>
|
|
35
|
+
<div className="nav-links">
|
|
36
|
+
{NAV_ITEMS.map((item) => (<react_router_dom_1.NavLink key={item.path} to={item.path} end={item.path === '/'} className={({ isActive }) => `nav-link${isActive ? ' active' : ''}`}>
|
|
37
|
+
{item.icon} {item.label}
|
|
38
|
+
</react_router_dom_1.NavLink>))}
|
|
39
|
+
</div>
|
|
40
|
+
<div className="nav-footer">
|
|
41
|
+
<span className={`status-dot ${statusClass}`}/>
|
|
42
|
+
<span className="status-text">{statusText}</span>
|
|
43
|
+
<span className="last-updated">{lastUpdated}</span>
|
|
44
|
+
</div>
|
|
45
|
+
</nav>
|
|
46
|
+
<main className="main">
|
|
47
|
+
{children}
|
|
48
|
+
</main>
|
|
49
|
+
</div>);
|
|
50
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Markdown — chat answer renderer (Phase 1 of DASHBOARD_FIRST_PLAN).
|
|
4
|
+
*
|
|
5
|
+
* Stack (decision reviewed 2026-08-17 with the user): react-markdown v10 +
|
|
6
|
+
* remark-gfm + rehype-highlight. The earlier custom renderer covered ~90% of
|
|
7
|
+
* CommonMark but mangled real agent output (nested lists, ***bold italic***,
|
|
8
|
+
* autolinks, strikethrough, task lists) and had NO syntax highlighting — the
|
|
9
|
+
* user's exact concern ("is your custom a weak fix?"). react-markdown is the
|
|
10
|
+
* spec-complete, battle-tested standard; it adds ~10 KB gzipped to a 219 KB
|
|
11
|
+
* bundle (<5%) and builds React elements (no dangerouslySetInnerHTML) with
|
|
12
|
+
* built-in URL sanitization — the same security model as before.
|
|
13
|
+
*
|
|
14
|
+
* The zero-dependency custom renderer is preserved as `MarkdownZeroDep.tsx`
|
|
15
|
+
* (unused by the app) for anyone who wants a dependency-free build — switch
|
|
16
|
+
* the import in ChatPage.tsx to use it.
|
|
17
|
+
*
|
|
18
|
+
* Supported: full CommonMark + GFM (tables, strikethrough, task lists,
|
|
19
|
+
* autolinks), syntax-highlighted fenced code blocks with a copy button,
|
|
20
|
+
* headings, lists, blockquotes, links (react-markdown's defaultUrlTransform
|
|
21
|
+
* strips javascript:/data: hrefs).
|
|
22
|
+
*/
|
|
23
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
24
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
25
|
+
};
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.default = Markdown;
|
|
28
|
+
const react_1 = require("react");
|
|
29
|
+
const react_markdown_1 = __importDefault(require("react-markdown"));
|
|
30
|
+
const remark_gfm_1 = __importDefault(require("remark-gfm"));
|
|
31
|
+
const rehype_highlight_1 = __importDefault(require("rehype-highlight"));
|
|
32
|
+
const typescript_1 = __importDefault(require("highlight.js/lib/languages/typescript"));
|
|
33
|
+
const javascript_1 = __importDefault(require("highlight.js/lib/languages/javascript"));
|
|
34
|
+
const json_1 = __importDefault(require("highlight.js/lib/languages/json"));
|
|
35
|
+
const bash_1 = __importDefault(require("highlight.js/lib/languages/bash"));
|
|
36
|
+
const shell_1 = __importDefault(require("highlight.js/lib/languages/shell"));
|
|
37
|
+
const python_1 = __importDefault(require("highlight.js/lib/languages/python"));
|
|
38
|
+
const markdown_1 = __importDefault(require("highlight.js/lib/languages/markdown"));
|
|
39
|
+
const yaml_1 = __importDefault(require("highlight.js/lib/languages/yaml"));
|
|
40
|
+
const diff_1 = __importDefault(require("highlight.js/lib/languages/diff"));
|
|
41
|
+
const sql_1 = __importDefault(require("highlight.js/lib/languages/sql"));
|
|
42
|
+
const xml_1 = __importDefault(require("highlight.js/lib/languages/xml"));
|
|
43
|
+
const css_1 = __importDefault(require("highlight.js/lib/languages/css"));
|
|
44
|
+
const plaintext_1 = __importDefault(require("highlight.js/lib/languages/plaintext"));
|
|
45
|
+
/**
|
|
46
|
+
* Curated highlight languages — the set the agent actually emits (ts, js,
|
|
47
|
+
* json, bash, python, markdown, yaml, diff, sql, html/xml, css). rehype-
|
|
48
|
+
* highlight's DEFAULT lowlight bundle carries ~37 grammars; registering only
|
|
49
|
+
* these keeps the dashboard bundle ~80 KB smaller (measured: gzip 322 KB →
|
|
50
|
+
* ~240 KB) while still highlighting every real code block. rehype-highlight
|
|
51
|
+
* expects a name → grammar RECORD (it builds its own lowlight internally).
|
|
52
|
+
*/
|
|
53
|
+
const HIGHLIGHT_LANGUAGES = {
|
|
54
|
+
ts: typescript_1.default,
|
|
55
|
+
typescript: typescript_1.default,
|
|
56
|
+
js: javascript_1.default,
|
|
57
|
+
javascript: javascript_1.default,
|
|
58
|
+
json: json_1.default,
|
|
59
|
+
bash: bash_1.default,
|
|
60
|
+
shell: shell_1.default,
|
|
61
|
+
sh: shell_1.default,
|
|
62
|
+
python: python_1.default,
|
|
63
|
+
py: python_1.default,
|
|
64
|
+
markdown: markdown_1.default,
|
|
65
|
+
md: markdown_1.default,
|
|
66
|
+
yaml: yaml_1.default,
|
|
67
|
+
yml: yaml_1.default,
|
|
68
|
+
diff: diff_1.default,
|
|
69
|
+
sql: sql_1.default,
|
|
70
|
+
html: xml_1.default,
|
|
71
|
+
xml: xml_1.default,
|
|
72
|
+
css: css_1.default,
|
|
73
|
+
text: plaintext_1.default,
|
|
74
|
+
plaintext: plaintext_1.default,
|
|
75
|
+
};
|
|
76
|
+
/** Walk a hast node to its raw text (highlight spans included). */
|
|
77
|
+
function hastText(node) {
|
|
78
|
+
if (node == null)
|
|
79
|
+
return '';
|
|
80
|
+
const n = node;
|
|
81
|
+
if (typeof n.value === 'string')
|
|
82
|
+
return n.value;
|
|
83
|
+
if (Array.isArray(n.children))
|
|
84
|
+
return n.children.map(hastText).join('');
|
|
85
|
+
return '';
|
|
86
|
+
}
|
|
87
|
+
/** A fenced code block: language label + copy button + highlighted body. */
|
|
88
|
+
function CodeBlockView({ lang, code, children }) {
|
|
89
|
+
const [copied, setCopied] = (0, react_1.useState)(false);
|
|
90
|
+
const copy = async () => {
|
|
91
|
+
try {
|
|
92
|
+
await navigator.clipboard?.writeText(code);
|
|
93
|
+
setCopied(true);
|
|
94
|
+
setTimeout(() => setCopied(false), 1500);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
/* clipboard unavailable (non-secure context / jsdom) — button no-ops */
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
return (<div className="md-code-block">
|
|
101
|
+
<div className="md-code-head">
|
|
102
|
+
<span className="md-code-lang">{lang}</span>
|
|
103
|
+
<button type="button" className="md-code-copy" onClick={() => void copy()}>
|
|
104
|
+
{copied ? '✓ Copied' : '⧉ Copy'}
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
<pre className="md-code-pre">
|
|
108
|
+
<code>{children}</code>
|
|
109
|
+
</pre>
|
|
110
|
+
</div>);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Custom `pre` renderer: wraps every fenced block in the copy-button shell.
|
|
114
|
+
* `node` is the hast element — walk it for the RAW code text (rehype-highlight
|
|
115
|
+
* has already split children into spans), and read `language-*` for the label.
|
|
116
|
+
* Blocks without a language still get the shell (lang "text").
|
|
117
|
+
*/
|
|
118
|
+
function Pre({ node, children }) {
|
|
119
|
+
const codeNode = node?.children?.[0];
|
|
120
|
+
const classes = Array.isArray(codeNode?.properties?.className)
|
|
121
|
+
? codeNode?.properties?.className
|
|
122
|
+
: [];
|
|
123
|
+
const lang = classes.find((c) => typeof c === 'string' && c.startsWith('language-'))?.slice(9) ?? 'text';
|
|
124
|
+
const raw = (0, react_1.useMemo)(() => hastText(codeNode), [codeNode]);
|
|
125
|
+
return (<CodeBlockView lang={lang} code={raw}>
|
|
126
|
+
{children}
|
|
127
|
+
</CodeBlockView>);
|
|
128
|
+
}
|
|
129
|
+
/** Top-level renderer: react-markdown with GFM + highlighting + the shell. */
|
|
130
|
+
function Markdown({ text }) {
|
|
131
|
+
const components = (0, react_1.useMemo)(() => ({
|
|
132
|
+
h1: ({ node: _n, ...p }) => <h1 className="md-heading md-h1" {...p}/>,
|
|
133
|
+
h2: ({ node: _n, ...p }) => <h2 className="md-heading md-h2" {...p}/>,
|
|
134
|
+
h3: ({ node: _n, ...p }) => <h3 className="md-heading md-h3" {...p}/>,
|
|
135
|
+
h4: ({ node: _n, ...p }) => <h4 className="md-heading md-h4" {...p}/>,
|
|
136
|
+
h5: ({ node: _n, ...p }) => <h5 className="md-heading md-h5" {...p}/>,
|
|
137
|
+
h6: ({ node: _n, ...p }) => <h6 className="md-heading md-h6" {...p}/>,
|
|
138
|
+
p: ({ node: _n, ...p }) => <p className="md-para" {...p}/>,
|
|
139
|
+
ul: ({ node: _n, ...p }) => <ul className="md-list md-ul" {...p}/>,
|
|
140
|
+
ol: ({ node: _n, ...p }) => <ol className="md-list md-ol" {...p}/>,
|
|
141
|
+
blockquote: ({ node: _n, ...p }) => <blockquote className="md-quote" {...p}/>,
|
|
142
|
+
hr: ({ node: _n, ...p }) => <hr className="md-hr" {...p}/>,
|
|
143
|
+
table: ({ node: _n, ...p }) => <table className="md-table" {...p}/>,
|
|
144
|
+
a: ({ node: _n, ...p }) => <a target="_blank" rel="noreferrer" {...p}/>,
|
|
145
|
+
pre: Pre,
|
|
146
|
+
}), []);
|
|
147
|
+
return (<div className="md-root">
|
|
148
|
+
<react_markdown_1.default remarkPlugins={[remark_gfm_1.default]} rehypePlugins={[[rehype_highlight_1.default, { languages: HIGHLIGHT_LANGUAGES }]]} components={components}>
|
|
149
|
+
{text ?? ''}
|
|
150
|
+
</react_markdown_1.default>
|
|
151
|
+
</div>);
|
|
152
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Phase 1 — Markdown renderer tests (react-markdown + remark-gfm +
|
|
4
|
+
* rehype-highlight stack).
|
|
5
|
+
*
|
|
6
|
+
* The chat answer renderer must turn the agent's markdown into rich blocks:
|
|
7
|
+
* headings, lists, bold/italic, inline code, fenced code blocks with a copy
|
|
8
|
+
* button, GFM tables/strikethrough/task lists, autolinks — while NEVER
|
|
9
|
+
* injecting unsafe hrefs (react-markdown's defaultUrlTransform strips
|
|
10
|
+
* javascript:/data:). The zero-dep fallback (`MarkdownZeroDep`) gets a smoke
|
|
11
|
+
* test so the one-import switch stays verified.
|
|
12
|
+
*/
|
|
13
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
14
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
const vitest_1 = require("vitest");
|
|
18
|
+
const react_1 = require("@testing-library/react");
|
|
19
|
+
const Markdown_1 = __importDefault(require("./Markdown"));
|
|
20
|
+
const MarkdownZeroDep_1 = __importDefault(require("./MarkdownZeroDep"));
|
|
21
|
+
(0, vitest_1.afterEach)(() => {
|
|
22
|
+
(0, react_1.cleanup)();
|
|
23
|
+
vitest_1.vi.restoreAllMocks();
|
|
24
|
+
});
|
|
25
|
+
(0, vitest_1.describe)('Markdown (react-markdown stack)', () => {
|
|
26
|
+
(0, vitest_1.it)('renders plain prose as paragraphs', () => {
|
|
27
|
+
const { container } = (0, react_1.render)(<Markdown_1.default text={'hello world\n\nsecond para'}/>);
|
|
28
|
+
(0, vitest_1.expect)(container.querySelectorAll('p.md-para')).toHaveLength(2);
|
|
29
|
+
(0, vitest_1.expect)(react_1.screen.getByText('hello world')).toBeTruthy();
|
|
30
|
+
(0, vitest_1.expect)(react_1.screen.getByText('second para')).toBeTruthy();
|
|
31
|
+
});
|
|
32
|
+
(0, vitest_1.it)('renders headings at the right level', () => {
|
|
33
|
+
const { container } = (0, react_1.render)(<Markdown_1.default text={'# Big\n\n### Small'}/>);
|
|
34
|
+
(0, vitest_1.expect)(container.querySelector('h1')?.textContent).toBe('Big');
|
|
35
|
+
(0, vitest_1.expect)(container.querySelector('h3')?.textContent).toBe('Small');
|
|
36
|
+
});
|
|
37
|
+
(0, vitest_1.it)('renders bold, italic, inline code and links', () => {
|
|
38
|
+
const { container } = (0, react_1.render)(<Markdown_1.default text={'**bold** and *italic* and `code` and [docs](https://example.com)'}/>);
|
|
39
|
+
(0, vitest_1.expect)(container.querySelector('strong')?.textContent).toBe('bold');
|
|
40
|
+
(0, vitest_1.expect)(container.querySelector('em')?.textContent).toBe('italic');
|
|
41
|
+
(0, vitest_1.expect)(container.querySelector(':not(pre) > code')?.textContent).toBe('code');
|
|
42
|
+
(0, vitest_1.expect)(container.querySelector('a')?.getAttribute('href')).toBe('https://example.com');
|
|
43
|
+
});
|
|
44
|
+
(0, vitest_1.it)('never turns a javascript: link into a clickable href (defaultUrlTransform)', () => {
|
|
45
|
+
const { container } = (0, react_1.render)(<Markdown_1.default text={'[click](javascript:alert(1))'}/>);
|
|
46
|
+
// v10 renders the text with an EMPTY href — never a javascript: URL.
|
|
47
|
+
const a = container.querySelector('a');
|
|
48
|
+
(0, vitest_1.expect)(a?.getAttribute('href') ?? '').not.toMatch(/^javascript:/);
|
|
49
|
+
(0, vitest_1.expect)(react_1.screen.getByText('click')).toBeTruthy();
|
|
50
|
+
});
|
|
51
|
+
(0, vitest_1.it)('renders bullet and numbered lists as groups', () => {
|
|
52
|
+
const { container } = (0, react_1.render)(<Markdown_1.default text={'- one\n- two\n- three'}/>);
|
|
53
|
+
const ul = container.querySelector('ul.md-list');
|
|
54
|
+
(0, vitest_1.expect)(ul).toBeTruthy();
|
|
55
|
+
(0, vitest_1.expect)(ul?.querySelectorAll('li')).toHaveLength(3);
|
|
56
|
+
const { container: c2 } = (0, react_1.render)(<Markdown_1.default text={'1. first\n2. second'}/>);
|
|
57
|
+
(0, vitest_1.expect)(c2.querySelector('ol.md-list')).toBeTruthy();
|
|
58
|
+
});
|
|
59
|
+
(0, vitest_1.it)('handles nested lists (CommonMark depth the old renderer could not)', () => {
|
|
60
|
+
const { container } = (0, react_1.render)(<Markdown_1.default text={'- outer\n - inner\n- again'}/>);
|
|
61
|
+
const ul = container.querySelector('ul.md-list');
|
|
62
|
+
// The inner list is a real nested <ul>, not flattened text.
|
|
63
|
+
(0, vitest_1.expect)(ul?.querySelector('ul')?.querySelector('li')?.textContent).toBe('inner');
|
|
64
|
+
(0, vitest_1.expect)(react_1.screen.getByText('outer')).toBeTruthy();
|
|
65
|
+
(0, vitest_1.expect)(react_1.screen.getByText('again')).toBeTruthy();
|
|
66
|
+
});
|
|
67
|
+
(0, vitest_1.it)('renders GFM strikethrough, task lists and autolinks', () => {
|
|
68
|
+
const { container } = (0, react_1.render)(<Markdown_1.default text={'~~gone~~\n\n- [x] done\n- [ ] todo\n\nvisit <https://example.com> now'}/>);
|
|
69
|
+
(0, vitest_1.expect)(container.querySelector('del')?.textContent).toBe('gone');
|
|
70
|
+
// Task-list checkboxes are real inputs.
|
|
71
|
+
(0, vitest_1.expect)(container.querySelectorAll('input[type="checkbox"]')).toHaveLength(2);
|
|
72
|
+
(0, vitest_1.expect)(container.querySelector('input[type="checkbox"]')?.hasAttribute('checked')).toBe(true);
|
|
73
|
+
(0, vitest_1.expect)(container.querySelector('a')?.getAttribute('href')).toBe('https://example.com');
|
|
74
|
+
});
|
|
75
|
+
(0, vitest_1.it)('renders a fenced code block with a language label, syntax highlight and copy button', async () => {
|
|
76
|
+
const writeText = vitest_1.vi.fn().mockResolvedValue(undefined);
|
|
77
|
+
Object.defineProperty(navigator, 'clipboard', { value: { writeText }, configurable: true });
|
|
78
|
+
(0, react_1.render)(<Markdown_1.default text={'```ts\nconst x = 1;\n```'}/>);
|
|
79
|
+
(0, vitest_1.expect)(react_1.screen.getByText('ts')).toBeTruthy();
|
|
80
|
+
// rehype-highlight tokenized the body — the raw text is intact on the <code>.
|
|
81
|
+
const codeEl = document.querySelector('.md-code-pre code');
|
|
82
|
+
(0, vitest_1.expect)(codeEl?.textContent?.trim()).toBe('const x = 1;');
|
|
83
|
+
(0, vitest_1.expect)(codeEl?.querySelector('.hljs-keyword')?.textContent).toBe('const');
|
|
84
|
+
react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /Copy/ }));
|
|
85
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(writeText).toHaveBeenCalledWith('const x = 1;\n'));
|
|
86
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByText(/Copied/)).toBeTruthy());
|
|
87
|
+
});
|
|
88
|
+
(0, vitest_1.it)('keeps text inside code fences literal (no bold parsing)', () => {
|
|
89
|
+
const { container } = (0, react_1.render)(<Markdown_1.default text={'```\n**not bold**\n```'}/>);
|
|
90
|
+
(0, vitest_1.expect)(container.querySelector('strong')).toBeNull();
|
|
91
|
+
(0, vitest_1.expect)(react_1.screen.getByText('**not bold**')).toBeTruthy();
|
|
92
|
+
});
|
|
93
|
+
(0, vitest_1.it)('renders blockquotes and GFM tables', () => {
|
|
94
|
+
const { container } = (0, react_1.render)(<Markdown_1.default text={'> a note\n\n| a | b |\n|---|---|\n| 1 | 2 |'}/>);
|
|
95
|
+
(0, vitest_1.expect)(container.querySelector('blockquote')?.textContent?.trim()).toBe('a note');
|
|
96
|
+
const table = container.querySelector('table.md-table');
|
|
97
|
+
(0, vitest_1.expect)(table).toBeTruthy();
|
|
98
|
+
(0, vitest_1.expect)(table?.querySelectorAll('th')).toHaveLength(2);
|
|
99
|
+
(0, vitest_1.expect)(table?.querySelectorAll('td')).toHaveLength(2);
|
|
100
|
+
(0, vitest_1.expect)(react_1.screen.getByText('1')).toBeTruthy();
|
|
101
|
+
(0, vitest_1.expect)(react_1.screen.getByText('2')).toBeTruthy();
|
|
102
|
+
});
|
|
103
|
+
(0, vitest_1.it)('renders an empty string safely', () => {
|
|
104
|
+
const { container } = (0, react_1.render)(<Markdown_1.default text={''}/>);
|
|
105
|
+
const root = container.querySelector('.md-root');
|
|
106
|
+
(0, vitest_1.expect)(root).toBeTruthy();
|
|
107
|
+
(0, vitest_1.expect)(root?.querySelector('p, pre, table, ul, ol, blockquote, h1, h2, h3, h4, h5, h6')).toBeNull();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
(0, vitest_1.describe)('MarkdownZeroDep (zero-dependency fallback)', () => {
|
|
111
|
+
(0, vitest_1.it)('still renders prose and fenced code with a copy button', async () => {
|
|
112
|
+
const writeText = vitest_1.vi.fn().mockResolvedValue(undefined);
|
|
113
|
+
Object.defineProperty(navigator, 'clipboard', { value: { writeText }, configurable: true });
|
|
114
|
+
(0, react_1.render)(<MarkdownZeroDep_1.default text={'hello\n\n```js\nconsole.log(1)\n```'}/>);
|
|
115
|
+
(0, vitest_1.expect)(react_1.screen.getByText('hello')).toBeTruthy();
|
|
116
|
+
(0, vitest_1.expect)(react_1.screen.getByText('js')).toBeTruthy();
|
|
117
|
+
(0, vitest_1.expect)(react_1.screen.getByText('console.log(1)')).toBeTruthy();
|
|
118
|
+
react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /Copy/ }));
|
|
119
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(writeText).toHaveBeenCalledWith('console.log(1)'));
|
|
120
|
+
});
|
|
121
|
+
(0, vitest_1.it)('still strips unsafe link schemes', () => {
|
|
122
|
+
const { container } = (0, react_1.render)(<MarkdownZeroDep_1.default text={'[click](javascript:alert(1))'}/>);
|
|
123
|
+
(0, vitest_1.expect)(container.querySelector('a')).toBeNull();
|
|
124
|
+
(0, vitest_1.expect)(react_1.screen.getByText('click')).toBeTruthy();
|
|
125
|
+
});
|
|
126
|
+
});
|