Compare commits

...
Author SHA1 Message Date
MervinPraisonandClaude Opus 5.5 ef5ed0ae62 fix: guard session rail against non-array session payloads
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-04 12:20:19 +00:00
MervinPraisonandClaude Opus 5.5 42c0a48640 fix: add in-rail session search to chat sidebar (fixes #324)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-04 11:45:03 +00:00
Mervin PraisonandClaude Opus 5.5 7002e956fb ci: bump github-automation-template to v1.1.20 (#320)
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-29 02:38:22 +01:00
9 changed files with 105 additions and 52 deletions

No files matched your search

+2 -2
View File
@@ -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
+2 -2
View File
@@ -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 || '' }}
+2 -2
View File
@@ -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 || '' }}
+4 -4
View File
@@ -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' }}
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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:
+2 -2
View File
@@ -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
@@ -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();
@@ -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