mirror of
https://github.com/MervinPraison/PraisonAIUI.git
synced 2026-11-07 05:56:17 -10:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef5ed0ae62 | ||
|
|
42c0a48640 | ||
|
|
7002e956fb | ||
|
|
ebbb5f863e | ||
|
|
34a01c1738 | ||
|
|
0d2b7b1d09 | ||
|
|
4ab1eee24f | ||
|
|
a89b00da5b |
No files matched your search
@@ -27,7 +27,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].19
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].20
|
||||
secrets: inherit
|
||||
with:
|
||||
hub_ref: v1.1.19
|
||||
hub_ref: v1.1.20
|
||||
@@ -16,8 +16,8 @@ permissions:
|
||||
|
||||
jobs:
|
||||
recover:
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].19
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].20
|
||||
secrets: inherit
|
||||
with:
|
||||
hub_ref: v1.1.19
|
||||
hub_ref: v1.1.20
|
||||
pr_number: ${{ github.event.inputs.pr_number || '' }}
|
||||
@@ -25,8 +25,8 @@ permissions:
|
||||
|
||||
jobs:
|
||||
detect-and-trigger:
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].19
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].20
|
||||
secrets: inherit
|
||||
with:
|
||||
hub_ref: v1.1.19
|
||||
hub_ref: v1.1.20
|
||||
pr_number: ${{ github.event.inputs.pr_number || '' }}
|
||||
@@ -43,9 +43,9 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].19
|
||||
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].20
|
||||
with:
|
||||
hub_ref: v1.1.19
|
||||
hub_ref: v1.1.20
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
@@ -88,9 +88,9 @@ jobs:
|
||||
actions: read
|
||||
id-token: write
|
||||
statuses: write # the assess job records security/outsider-review
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].19
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].20
|
||||
secrets: inherit
|
||||
with:
|
||||
hub_ref: v1.1.19
|
||||
hub_ref: v1.1.20
|
||||
pr_number: ${{ github.event.inputs.pr_number || github.event.client_payload.pr_number || '' }}
|
||||
merge_method: ${{ github.event.inputs.merge_method || 'merge' }}
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
issues: write
|
||||
actions: read
|
||||
id-token: write
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].19
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].20
|
||||
secrets: inherit
|
||||
with:
|
||||
hub_ref: v1.1.19
|
||||
hub_ref: v1.1.20
|
||||
@@ -25,9 +25,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].19
|
||||
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].20
|
||||
with:
|
||||
hub_ref: v1.1.19
|
||||
hub_ref: v1.1.20
|
||||
- name: Detect conflicting PRs and trigger Claude
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
|
||||
@@ -13,7 +13,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].19
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].20
|
||||
secrets: inherit
|
||||
with:
|
||||
hub_ref: v1.1.19
|
||||
hub_ref: v1.1.20
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "aiui"
|
||||
version = "0.3.148"
|
||||
version = "0.3.149"
|
||||
description = "YAML-driven website generator - CLI and compiler"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""Version information."""
|
||||
|
||||
__version__ = "0.3.148"
|
||||
__version__ = "0.3.149"
|
||||
@@ -19,6 +19,7 @@ let isStreaming = false;
|
||||
let streamingSessionId = null; // tracks which session is actively streaming
|
||||
let currentRunId = null;
|
||||
let agents = [];
|
||||
let allSessions = []; // last fetched session list, filtered client-side by the rail search
|
||||
let messageQueue = [];
|
||||
let focusMode = false;
|
||||
let sessionSelectHandler = null;
|
||||
@@ -230,6 +231,8 @@ function injectStyles() {
|
||||
.chat-sidebar-agent { padding:10px 16px; border-bottom:1px solid var(--db-border); }
|
||||
.chat-sidebar-agent label { font-size:11px; color:var(--db-text-dim); display:block; margin-bottom:4px; text-transform:uppercase; font-weight:600; letter-spacing:.5px; }
|
||||
.chat-sidebar-agent select { width:100%; padding:6px 10px; background:var(--db-card-bg); border:1px solid var(--db-border); border-radius:6px; color:var(--db-text); font-size:13px; }
|
||||
.chat-sess-search { padding:8px 16px 0; }
|
||||
.chat-sess-search input { width:100%; box-sizing:border-box; padding:6px 10px; background:var(--db-card-bg); border:1px solid var(--db-border); border-radius:6px; color:var(--db-text); font-size:13px; }
|
||||
.chat-sess-list { flex:1; overflow-y:auto; padding:8px; }
|
||||
.chat-sess-item { padding:10px 12px; border-radius:8px; cursor:pointer; margin-bottom:4px; font-size:13px; transition:background .15s; display:flex; flex-direction:column; gap:2px; }
|
||||
.chat-sess-item:hover { background:rgba(var(--db-accent-rgb,100,100,255),.08); }
|
||||
@@ -812,6 +815,9 @@ export async function render(container) {
|
||||
<label>Agent</label>
|
||||
<select id="chat-agent-selector"><option value="">Auto (default)</option></select>
|
||||
</div>
|
||||
<div class="chat-sess-search">
|
||||
<input type="search" id="chat-session-search" placeholder="Search sessions…" aria-label="Search sessions" autocomplete="off">
|
||||
</div>
|
||||
<div class="chat-sess-list" id="chat-sessions-list"></div>
|
||||
</div>
|
||||
<div class="chat-main-panel">
|
||||
@@ -892,6 +898,8 @@ export async function render(container) {
|
||||
const agentSelector = document.getElementById('chat-agent-selector');
|
||||
const attachBtn = document.getElementById('chat-attach-btn');
|
||||
const fileInput = document.getElementById('chat-file-input');
|
||||
const sessionSearch = document.getElementById('chat-session-search');
|
||||
if (sessionSearch) sessionSearch.addEventListener('input', renderSessionList);
|
||||
// Guard: if DOM elements aren't present yet (race condition), bail out
|
||||
if (!input || !sendBtn) {
|
||||
console.warn('[chat] DOM elements not ready — skipping event binding');
|
||||
@@ -1039,47 +1047,61 @@ async function loadSessions() {
|
||||
resp = await fetch('/sessions');
|
||||
}
|
||||
const data = await resp.json();
|
||||
const sessions = data.sessions || data || [];
|
||||
const list = document.getElementById('chat-sessions-list');
|
||||
if (!list) return;
|
||||
list.innerHTML = '';
|
||||
if (sessions.length === 0) {
|
||||
list.innerHTML = '<div style="padding:16px;text-align:center;color:var(--db-text-dim);font-size:12px">No sessions yet</div>';
|
||||
return;
|
||||
}
|
||||
sessions.slice(0, 30).forEach((s) => {
|
||||
const el = document.createElement('div');
|
||||
const id = s.id || s.session_id || '';
|
||||
el.className = 'chat-sess-item' + (id === currentSessionId ? ' active' : '');
|
||||
// Show platform icon for channel sessions (e.g. Slack with SVG)
|
||||
const platformName = s.platform || '';
|
||||
const platSvg = _getPlatformSVG(platformName);
|
||||
let title;
|
||||
let titlePrefix = '';
|
||||
if (platformName) {
|
||||
const displayName = platformName.charAt(0).toUpperCase() + platformName.slice(1);
|
||||
if (platSvg) {
|
||||
titlePrefix = '<span class="sess-platform-icon ' + _getPlatformClass(platformName) + '">' + platSvg + '</span>';
|
||||
}
|
||||
title = displayName;
|
||||
} else {
|
||||
title = s.name || s.title || s.label || id.substring(0, 12) + '…';
|
||||
}
|
||||
const agent = s.agent_id || s.agent || '';
|
||||
const time = timeAgo(s.created_at || s.updated_at);
|
||||
el.innerHTML = `
|
||||
<div class="sess-title">${titlePrefix}${escapeHtml(title)}</div>
|
||||
<div class="sess-meta">${agent ? '🤖 ' + escapeHtml(agent) + ' · ' : ''}${time}</div>
|
||||
`;
|
||||
el.title = id;
|
||||
el.addEventListener('click', () => loadSession(id));
|
||||
list.appendChild(el);
|
||||
});
|
||||
const fetched = data.sessions || data || [];
|
||||
allSessions = Array.isArray(fetched) ? fetched : [];
|
||||
renderSessionList();
|
||||
} catch (e) {
|
||||
console.warn('[Chat] Failed to load sessions:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function _sessionMatches(s, q) {
|
||||
const fields = [s.id, s.session_id, s.name, s.title, s.label, s.agent_id, s.agent, s.platform];
|
||||
return fields.some((v) => typeof v === 'string' && v.toLowerCase().includes(q));
|
||||
}
|
||||
|
||||
function renderSessionList() {
|
||||
const list = document.getElementById('chat-sessions-list');
|
||||
if (!list) return;
|
||||
const input = document.getElementById('chat-session-search');
|
||||
const q = (input && input.value || '').trim().toLowerCase();
|
||||
const sessions = q ? allSessions.filter((s) => _sessionMatches(s, q)) : allSessions;
|
||||
list.innerHTML = '';
|
||||
if (sessions.length === 0) {
|
||||
const msg = q ? 'No matching sessions' : 'No sessions yet';
|
||||
list.innerHTML = '<div style="padding:16px;text-align:center;color:var(--db-text-dim);font-size:12px">' + msg + '</div>';
|
||||
return;
|
||||
}
|
||||
sessions.slice(0, 30).forEach((s) => {
|
||||
const el = document.createElement('div');
|
||||
const id = s.id || s.session_id || '';
|
||||
el.className = 'chat-sess-item' + (id === currentSessionId ? ' active' : '');
|
||||
// Show platform icon for channel sessions (e.g. Slack with SVG)
|
||||
const platformName = s.platform || '';
|
||||
const platSvg = _getPlatformSVG(platformName);
|
||||
let title;
|
||||
let titlePrefix = '';
|
||||
if (platformName) {
|
||||
const displayName = platformName.charAt(0).toUpperCase() + platformName.slice(1);
|
||||
if (platSvg) {
|
||||
titlePrefix = '<span class="sess-platform-icon ' + _getPlatformClass(platformName) + '">' + platSvg + '</span>';
|
||||
}
|
||||
title = displayName;
|
||||
} else {
|
||||
title = s.name || s.title || s.label || id.substring(0, 12) + '…';
|
||||
}
|
||||
const agent = s.agent_id || s.agent || '';
|
||||
const time = timeAgo(s.created_at || s.updated_at);
|
||||
el.innerHTML = `
|
||||
<div class="sess-title">${titlePrefix}${escapeHtml(title)}</div>
|
||||
<div class="sess-meta">${agent ? '🤖 ' + escapeHtml(agent) + ' · ' : ''}${time}</div>
|
||||
`;
|
||||
el.title = id;
|
||||
el.addEventListener('click', () => loadSession(id));
|
||||
list.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadSession(sessionId) {
|
||||
currentSessionId = sessionId;
|
||||
refreshStreamingUI();
|
||||
|
||||
@@ -51,6 +51,9 @@ const PAIRS = {
|
||||
};
|
||||
const END_OF = Object.fromEntries(Object.entries(PAIRS).map(([s, e]) => [e, s]));
|
||||
|
||||
/** Events an agent run emits for itself (lifecycle, model and tool calls). */
|
||||
const AGENT_RUN_EVENTS = new Set(['agent_start', 'agent_end', 'llm_request', 'llm_response', 'tool_call_start', 'tool_call_end']);
|
||||
|
||||
/** Approvers that mean "a person decided", anything else is an automatic source. */
|
||||
const HUMAN_APPROVERS = new Set(['user', 'human', 'you', 'ui', 'replay', 'console', 'web', 'dashboard']);
|
||||
|
||||
@@ -172,7 +175,7 @@ export function narrate(ev, related = null) {
|
||||
if (!ev || typeof ev !== 'object') return '';
|
||||
const d = dataOf(ev);
|
||||
const rd = dataOf(related);
|
||||
const who = str(ev.agent_name) || 'The run';
|
||||
const who = str(ev.agent_name) || (AGENT_RUN_EVENTS.has(ev.event_type) ? agentOf(ev) : '') || 'The run';
|
||||
switch (ev.event_type) {
|
||||
case 'session_start':
|
||||
return 'Recording started.';
|
||||
@@ -699,14 +702,32 @@ export function buildModel(events, graph = null) {
|
||||
// ── graph helpers ─────────────────────────────────────────────────────
|
||||
|
||||
/** Graph node ids an event touches (node_id, else derived from agent/tool names). */
|
||||
/**
|
||||
* The agent an event belongs to. An unnamed agent (``Agent(instructions=...)``)
|
||||
* has no agent_name: newer traces give it the id ``agent:Agent``, older ones
|
||||
* ``tool:None/<tool>`` or nothing, so its own run events fall back to "Agent".
|
||||
*/
|
||||
export function agentOf(ev) {
|
||||
if (!ev) return null;
|
||||
if (ev.agent_name) return ev.agent_name;
|
||||
const id = str(ev.node_id);
|
||||
if (id.startsWith('agent:')) return id.slice(6) || 'Agent';
|
||||
if (id.startsWith('tool:') && AGENT_RUN_EVENTS.has(ev.event_type)) {
|
||||
const who = id.slice(5).split('/')[0];
|
||||
return who && who !== 'None' ? who : 'Agent';
|
||||
}
|
||||
return AGENT_RUN_EVENTS.has(ev.event_type) ? 'Agent' : null;
|
||||
}
|
||||
|
||||
export function nodeIdsForEvent(ev) {
|
||||
const out = [];
|
||||
if (!ev) return out;
|
||||
if (ev.node_id) out.push(ev.node_id);
|
||||
if (ev.node_id && !str(ev.node_id).startsWith('tool:None/')) out.push(ev.node_id);
|
||||
const d = dataOf(ev);
|
||||
if (ev.agent_name) {
|
||||
out.push(ev.agent_name === 'Manager' ? MANAGER_LANE : `agent:${ev.agent_name}`);
|
||||
if (d.tool_name) out.push(`tool:${ev.agent_name}/${d.tool_name}`);
|
||||
const who = agentOf(ev);
|
||||
if (who) {
|
||||
out.push(who === 'Manager' ? MANAGER_LANE : `agent:${who}`);
|
||||
if (d.tool_name) out.push(`tool:${who}/${d.tool_name}`);
|
||||
}
|
||||
if (String(ev.event_type).startsWith('approval_')) out.push(HUMAN_LANE);
|
||||
return [...new Set(out)];
|
||||
@@ -717,8 +738,11 @@ export function nodeIdsForEvent(ev) {
|
||||
* sessions): agents, their tools, the manager and you, with hand-off/uses/reviews
|
||||
* edges labelled with how often they happened.
|
||||
*/
|
||||
export function deriveGraph(events) {
|
||||
export function deriveGraph(events, opts = {}) {
|
||||
const evs = sortEvents(events);
|
||||
// Unnamed agents count only when there is no recorded graph: a team graph
|
||||
// leaves them out on purpose, and they must not reappear as "outside".
|
||||
const nameOf = opts.unnamed === false ? (ev) => ev.agent_name || null : agentOf;
|
||||
const nodes = new Map();
|
||||
const edges = new Map();
|
||||
const node = (id, kind, label) => { if (!nodes.has(id)) nodes.set(id, { id, kind, label, parent: null, meta: {} }); return id; };
|
||||
@@ -732,16 +756,17 @@ export function deriveGraph(events) {
|
||||
};
|
||||
evs.forEach((ev) => {
|
||||
const d = dataOf(ev);
|
||||
if (ev.agent_name) agentNode(ev.agent_name);
|
||||
if (ev.event_type === 'agent_handoff' && (d.from_agent || ev.agent_name) && d.to_agent) {
|
||||
const who = nameOf(ev);
|
||||
if (who) agentNode(who);
|
||||
if (ev.event_type === 'agent_handoff' && (d.from_agent || who) && d.to_agent) {
|
||||
const kind = { delegate: 'delegates', route: 'routes', spawn: 'handoff' }[d.kind] || 'handoff';
|
||||
edge(agentNode(d.from_agent || ev.agent_name), agentNode(d.to_agent), kind);
|
||||
} else if (ev.event_type === 'tool_call_start' && ev.agent_name && d.tool_name) {
|
||||
const tool = node(`tool:${ev.agent_name}/${d.tool_name}`, 'tool', d.tool_name);
|
||||
edge(agentNode(ev.agent_name), tool, 'uses');
|
||||
edge(agentNode(d.from_agent || who), agentNode(d.to_agent), kind);
|
||||
} else if (ev.event_type === 'tool_call_start' && who && d.tool_name) {
|
||||
const tool = node(`tool:${who}/${d.tool_name}`, 'tool', d.tool_name);
|
||||
edge(agentNode(who), tool, 'uses');
|
||||
} else if (ev.event_type === 'approval_requested') {
|
||||
const you = node(HUMAN_LANE, 'human', 'You');
|
||||
const target = d.tool && ev.agent_name ? node(`tool:${ev.agent_name}/${d.tool}`, 'tool', d.tool) : (ev.agent_name ? agentNode(ev.agent_name) : null);
|
||||
const target = d.tool && who ? node(`tool:${who}/${d.tool}`, 'tool', d.tool) : (who ? agentNode(who) : null);
|
||||
if (target) edge(you, target, 'reviews');
|
||||
}
|
||||
});
|
||||
@@ -929,7 +954,8 @@ export function layoutMap(graph, opts = {}) {
|
||||
* them as "Outside the team". Without a recorded graph this is deriveGraph().
|
||||
*/
|
||||
export function observedGraph(graph, events) {
|
||||
const derived = deriveGraph(events);
|
||||
const hasGraph = Boolean(graph && Array.isArray(graph.nodes) && graph.nodes.length);
|
||||
const derived = deriveGraph(events, { unnamed: !hasGraph });
|
||||
if (!graph || !Array.isArray(graph.nodes) || !graph.nodes.length) return derived;
|
||||
const nodes = graph.nodes.filter((n) => n && n.id).map((n) => ({ ...n, meta: { ...(n.meta || {}) } }));
|
||||
const edges = (Array.isArray(graph.edges) ? graph.edges : []).filter((e) => e && e.source && e.target).map((e) => ({ ...e }));
|
||||
@@ -1095,7 +1121,7 @@ export function edgeText(edge) {
|
||||
|
||||
const api = {
|
||||
PALETTE, SHAPES, HUMAN_LANE, MANAGER_LANE, EDGE_WORDS,
|
||||
buildModel, narrate, layoutMap, layoutRunMap, observedGraph, deriveGraph, nodeIdsForEvent, nodeStates, pairEvents, sortEvents,
|
||||
buildModel, narrate, layoutMap, layoutRunMap, observedGraph, deriveGraph, nodeIdsForEvent, agentOf, nodeStates, pairEvents, sortEvents,
|
||||
eventId, formatDuration, formatCost, argPreview, truncate, stripPrefix, edgeText,
|
||||
};
|
||||
if (typeof globalThis !== 'undefined') globalThis.PraisonReplayCore = api;
|
||||
|
||||
@@ -456,3 +456,54 @@ test('nodeStates: a recorded flow fixture ends with every visited node done or f
|
||||
assert.ok(st.size > 0);
|
||||
for (const [id, v] of st) assert.notEqual(v.state, 'running', `${id} still running after the run ended`);
|
||||
});
|
||||
|
||||
// ── unnamed agents (Agent(instructions=...), agent_name null) ────────────
|
||||
|
||||
const unnamedRun = (newIds) => [
|
||||
{ sequence_num: 0, timestamp: 1, event_type: 'session_start', agent_name: null },
|
||||
{ sequence_num: 1, timestamp: 2, event_type: 'agent_start', agent_name: null, node_id: newIds ? 'agent:Agent' : null },
|
||||
{ sequence_num: 2, timestamp: 3, event_type: 'llm_request', agent_name: null, node_id: newIds ? 'agent:Agent' : null },
|
||||
{ sequence_num: 3, timestamp: 4, event_type: 'llm_response', agent_name: null, node_id: newIds ? 'agent:Agent' : null },
|
||||
{ sequence_num: 4, timestamp: 5, event_type: 'tool_call_start', agent_name: null, node_id: newIds ? 'tool:Agent/search' : 'tool:None/search', data: { tool_name: 'search' } },
|
||||
{ sequence_num: 5, timestamp: 6, event_type: 'tool_call_end', agent_name: null, node_id: newIds ? 'tool:Agent/search' : 'tool:None/search', data: { tool_name: 'search' } },
|
||||
{ sequence_num: 6, timestamp: 7, event_type: 'agent_end', agent_name: null, node_id: newIds ? 'agent:Agent' : null, data: newIds ? { status: 'completed' } : {} },
|
||||
{ sequence_num: 7, timestamp: 8, event_type: 'session_end', agent_name: null },
|
||||
];
|
||||
|
||||
for (const newIds of [true, false]) {
|
||||
test(`unnamed agent is mapped as "Agent" (${newIds ? 'new' : '4.7.19'} trace ids)`, () => {
|
||||
const g = deriveGraph(unnamedRun(newIds));
|
||||
assert.deepEqual(g.nodes.map((n) => [n.id, n.label]), [['agent:Agent', 'Agent'], ['tool:Agent/search', 'search']]);
|
||||
assert.deepEqual(g.edges.map((e) => `${e.source}->${e.target}:${e.kind}`), ['agent:Agent->tool:Agent/search:uses']);
|
||||
const st = core.nodeStates(unnamedRun(newIds), Infinity, g.nodes);
|
||||
assert.equal(st.get('agent:Agent').state, 'done');
|
||||
assert.equal(st.get('tool:Agent/search').state, 'done');
|
||||
assert.ok(![...st.keys()].some((k) => k.includes('None')), 'no tool:None ids leak into the map');
|
||||
});
|
||||
}
|
||||
|
||||
test('a failed unnamed agent run is red on the map', () => {
|
||||
const evs = unnamedRun(true);
|
||||
evs[6] = { ...evs[6], data: { status: 'failed' } };
|
||||
assert.equal(core.nodeStates(evs, Infinity, deriveGraph(evs).nodes).get('agent:Agent').state, 'failed');
|
||||
});
|
||||
|
||||
test('unnamed agents do not reappear "outside" a recorded team graph', () => {
|
||||
const graph = { schema: 'praisonai.graph/1', kind: 'team', nodes: [
|
||||
{ id: 'start:begin', kind: 'start', label: 'Brief' }, { id: 'task:t', kind: 'task', label: 't' }, { id: 'end:done', kind: 'end', label: 'Delivered' }],
|
||||
edges: [{ source: 'start:begin', target: 'task:t', kind: 'next' }, { source: 'task:t', target: 'end:done', kind: 'next' }] };
|
||||
const og = core.observedGraph(graph, unnamedRun(true));
|
||||
assert.deepEqual(og.nodes.map((n) => n.id), ['start:begin', 'task:t', 'end:done']);
|
||||
assert.deepEqual(og.outside, []);
|
||||
});
|
||||
|
||||
test('named agents are unchanged', () => {
|
||||
const evs = unnamedRun(true).map((e) => ({ ...e, agent_name: e.agent_name === null && e.event_type !== 'session_start' && e.event_type !== 'session_end' ? 'Writer' : e.agent_name, node_id: e.node_id && e.node_id.replace('Agent', 'Writer') }));
|
||||
assert.deepEqual(deriveGraph(evs).nodes.map((n) => n.id), ['agent:Writer', 'tool:Writer/search']);
|
||||
});
|
||||
|
||||
test('an unnamed agent is narrated as "Agent", not "The run"', () => {
|
||||
const evs = unnamedRun(true);
|
||||
assert.equal(narrate(evs[1]), 'Agent started.');
|
||||
assert.equal(narrate(evs[7]).startsWith('Recording'), true);
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Tests for the in-rail session search in the chat view (issue #324).
|
||||
|
||||
The chat sidebar must expose a search input that filters the already
|
||||
fetched session list client-side, without refetching from the API.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
CHAT = Path("src/praisonaiui/templates/frontend/plugins/views/chat.js")
|
||||
|
||||
|
||||
class TestChatSessionRailSearch:
|
||||
def test_search_input_rendered_in_rail(self):
|
||||
src = CHAT.read_text()
|
||||
assert 'id="chat-session-search"' in src
|
||||
assert src.index('id="chat-session-search"') < src.index('id="chat-sessions-list"')
|
||||
|
||||
def test_input_wired_to_client_side_render(self):
|
||||
src = CHAT.read_text()
|
||||
assert "sessionSearch.addEventListener('input', renderSessionList)" in src
|
||||
|
||||
def test_load_sessions_caches_and_delegates_render(self):
|
||||
src = CHAT.read_text()
|
||||
assert "allSessions = Array.isArray(fetched) ? fetched : [];" in src
|
||||
body = src.split("async function loadSessions()", 1)[1].split("\n}\n", 1)[0]
|
||||
assert "renderSessionList();" in body
|
||||
|
||||
def test_filter_matches_title_and_id_fields(self):
|
||||
src = CHAT.read_text()
|
||||
assert "function _sessionMatches(s, q)" in src
|
||||
assert "No matching sessions" in src
|
||||
Reference in new issue
Block a user