Add Repositories and Apps tabs to Sources for @mention context in chat

- Added Repositories tab with GitHub/GitLab/Bitbucket support
- Added Apps tab for previously created HTMX apps
- @mention autocomplete in chat for repos (@botserver) and apps (@myapp)
- Task context storage for autonomous task execution
- CSS for repo and app cards with connection status
- Mention suggestions dropdown with keyboard navigation
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-12-12 23:45:56 -03:00
parent 88a2610a62
commit 664211d6db
3 changed files with 1296 additions and 296 deletions

View file

@ -8,7 +8,7 @@
<header class="sources-header"> <header class="sources-header">
<div class="header-left"> <div class="header-left">
<h1>Sources</h1> <h1>Sources</h1>
<p class="header-subtitle">Prompts, Templates, MCP Servers & AI Models</p> <p class="header-subtitle">Repositories, Apps, Prompts, Templates & MCP Servers</p>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="search-box"> <div class="search-box">
@ -32,6 +32,33 @@
<button class="tab-btn active" <button class="tab-btn active"
role="tab" role="tab"
aria-selected="true" aria-selected="true"
hx-get="/api/sources/repositories"
hx-target="#content-area"
hx-swap="innerHTML"
onclick="setActiveTab(this)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
</svg>
Repositories
</button>
<button class="tab-btn"
role="tab"
aria-selected="false"
hx-get="/api/sources/apps"
hx-target="#content-area"
hx-swap="innerHTML"
onclick="setActiveTab(this)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="7" height="7"></rect>
<rect x="14" y="3" width="7" height="7"></rect>
<rect x="14" y="14" width="7" height="7"></rect>
<rect x="3" y="14" width="7" height="7"></rect>
</svg>
Apps
</button>
<button class="tab-btn"
role="tab"
aria-selected="false"
hx-get="/api/sources/prompts" hx-get="/api/sources/prompts"
hx-target="#content-area" hx-target="#content-area"
hx-swap="innerHTML" hx-swap="innerHTML"
@ -120,7 +147,7 @@
</nav> </nav>
<!-- Main Content Area --> <!-- Main Content Area -->
<main class="content-area" id="content-area" hx-get="/api/sources/prompts" hx-trigger="load" hx-swap="innerHTML"> <main class="content-area" id="content-area" hx-get="/api/sources/repositories" hx-trigger="load" hx-swap="innerHTML">
<!-- Content loaded via HTMX --> <!-- Content loaded via HTMX -->
<div class="loading-spinner"> <div class="loading-spinner">
<div class="spinner"></div> <div class="spinner"></div>

View file

@ -525,7 +525,9 @@
} }
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to {
transform: rotate(360deg);
}
} }
/* Empty State */ /* Empty State */
@ -651,7 +653,402 @@
color: var(--text-secondary); color: var(--text-secondary);
} }
/* Repository Card */
.repo-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
transition: all 0.2s;
cursor: pointer;
}
.repo-card:hover {
border-color: var(--primary);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.repo-card.connected {
border-color: var(--success);
}
.repo-header {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
}
.repo-icon {
width: 40px;
height: 40px;
border-radius: 10px;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.repo-icon svg {
width: 20px;
height: 20px;
color: white;
}
.repo-icon.github {
background: linear-gradient(135deg, #24292e, #40464e);
}
.repo-icon.gitlab {
background: linear-gradient(135deg, #fc6d26, #e24329);
}
.repo-icon.bitbucket {
background: linear-gradient(135deg, #0052cc, #2684ff);
}
.repo-info {
flex: 1;
min-width: 0;
}
.repo-name {
font-size: 15px;
font-weight: 600;
margin: 0 0 4px 0;
display: flex;
align-items: center;
gap: 8px;
}
.repo-name .mention-tag {
font-size: 12px;
color: var(--primary);
background: var(--primary-bg);
padding: 2px 8px;
border-radius: 4px;
font-weight: 500;
}
.repo-owner {
font-size: 13px;
color: var(--text-secondary);
}
.repo-description {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.5;
margin-bottom: 12px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.repo-meta {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.repo-meta-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--text-secondary);
}
.repo-meta-item svg {
width: 14px;
height: 14px;
}
.repo-language {
display: flex;
align-items: center;
gap: 6px;
}
.repo-language-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.repo-language-dot.rust {
background: #dea584;
}
.repo-language-dot.typescript {
background: #3178c6;
}
.repo-language-dot.javascript {
background: #f7df1e;
}
.repo-language-dot.python {
background: #3776ab;
}
.repo-language-dot.html {
background: #e34f26;
}
.repo-language-dot.css {
background: #1572b6;
}
.repo-status {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
padding: 4px 10px;
border-radius: 12px;
font-weight: 500;
}
.repo-status.connected {
background: var(--success-bg);
color: var(--success);
}
.repo-status.disconnected {
background: var(--surface-hover);
color: var(--text-secondary);
}
.repo-actions {
display: flex;
gap: 8px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--border);
}
.repo-action-btn {
flex: 1;
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 6px;
background: transparent;
color: var(--text);
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.repo-action-btn:hover {
background: var(--surface-hover);
border-color: var(--primary);
}
.repo-action-btn.primary {
background: var(--primary);
border-color: var(--primary);
color: white;
}
.repo-action-btn.primary:hover {
background: var(--primary-hover);
}
/* App Card */
.app-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
transition: all 0.2s;
cursor: pointer;
}
.app-card:hover {
border-color: var(--primary);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.app-preview {
height: 140px;
background: linear-gradient(135deg, var(--surface-hover), var(--surface));
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.app-preview-placeholder {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
color: var(--text-secondary);
opacity: 0.5;
}
.app-preview-placeholder svg {
width: 40px;
height: 40px;
}
.app-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.app-content {
padding: 16px;
}
.app-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 8px;
}
.app-name {
font-size: 15px;
font-weight: 600;
margin: 0;
display: flex;
align-items: center;
gap: 8px;
}
.app-name .mention-tag {
font-size: 11px;
color: var(--primary);
background: var(--primary-bg);
padding: 2px 6px;
border-radius: 4px;
font-weight: 500;
}
.app-type {
font-size: 11px;
padding: 4px 8px;
border-radius: 4px;
font-weight: 500;
background: var(--surface-hover);
color: var(--text-secondary);
}
.app-type.htmx {
background: #e8f4fd;
color: #0284c7;
}
.app-type.site {
background: #fef3c7;
color: #d97706;
}
.app-type.dashboard {
background: #dcfce7;
color: #16a34a;
}
.app-description {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.5;
margin-bottom: 12px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.app-meta {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
color: var(--text-secondary);
}
.app-created {
display: flex;
align-items: center;
gap: 4px;
}
.app-url {
color: var(--primary);
font-family: var(--font-mono);
font-size: 11px;
}
.app-actions {
display: flex;
gap: 8px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--border);
}
.app-action-btn {
flex: 1;
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 6px;
background: transparent;
color: var(--text);
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.app-action-btn:hover {
background: var(--surface-hover);
border-color: var(--primary);
}
.app-action-btn.primary {
background: var(--primary);
border-color: var(--primary);
color: white;
}
/* Add Repository Modal */
.add-repo-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.repo-url-input {
width: 100%;
padding: 12px 16px;
border: 1px solid var(--border);
border-radius: 8px;
font-size: 14px;
background: var(--surface);
color: var(--text);
}
.repo-url-input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-bg);
}
/* Grids for different content types */ /* Grids for different content types */
.repos-grid,
.apps-grid,
.templates-grid, .templates-grid,
.servers-grid, .servers-grid,
.models-grid, .models-grid,
@ -661,6 +1058,8 @@
gap: 16px; gap: 16px;
} }
.repos-grid.list-view,
.apps-grid.list-view,
.templates-grid.list-view, .templates-grid.list-view,
.servers-grid.list-view, .servers-grid.list-view,
.models-grid.list-view, .models-grid.list-view,
@ -703,6 +1102,8 @@
} }
.prompts-grid, .prompts-grid,
.repos-grid,
.apps-grid,
.templates-grid, .templates-grid,
.servers-grid, .servers-grid,
.models-grid, .models-grid,
@ -710,3 +1111,79 @@
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
/* Mention autocomplete in chat */
.mention-suggestion {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
cursor: pointer;
transition: background 0.15s;
}
.mention-suggestion:hover,
.mention-suggestion.active {
background: var(--surface-hover);
}
.mention-suggestion-icon {
width: 28px;
height: 28px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.mention-suggestion-icon.repo {
background: linear-gradient(135deg, #24292e, #40464e);
color: white;
}
.mention-suggestion-icon.app {
background: linear-gradient(135deg, #06b6d4, #0ea5e9);
color: white;
}
.mention-suggestion-info {
flex: 1;
min-width: 0;
}
.mention-suggestion-name {
font-size: 14px;
font-weight: 500;
margin: 0;
}
.mention-suggestion-type {
font-size: 12px;
color: var(--text-secondary);
}
/* Inline mention tag in chat */
.chat-mention {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
background: var(--primary-bg);
color: var(--primary);
border-radius: 4px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
}
.chat-mention:hover {
background: var(--primary);
color: white;
}
.chat-mention svg {
width: 14px;
height: 14px;
}

View file

@ -1,9 +1,10 @@
/** /**
* Sources Module JavaScript * Sources Module JavaScript
* Prompts, Templates, MCP Servers & AI Models * Repositories, Apps, Prompts, Templates, MCP Servers & AI Models
* Provides @mention support for chat context
*/ */
(function() { (function () {
'use strict'; "use strict";
/** /**
* Initialize the Sources module * Initialize the Sources module
@ -14,26 +15,29 @@
setupViewToggle(); setupViewToggle();
setupKeyboardShortcuts(); setupKeyboardShortcuts();
setupHTMXEvents(); setupHTMXEvents();
setupRepoCards();
setupAppCards();
setupMentionAutocomplete();
} }
/** /**
* Set active tab * Set active tab
*/ */
window.setActiveTab = function(btn) { window.setActiveTab = function (btn) {
document.querySelectorAll('.tab-btn').forEach(t => { document.querySelectorAll(".tab-btn").forEach((t) => {
t.classList.remove('active'); t.classList.remove("active");
t.setAttribute('aria-selected', 'false'); t.setAttribute("aria-selected", "false");
}); });
btn.classList.add('active'); btn.classList.add("active");
btn.setAttribute('aria-selected', 'true'); btn.setAttribute("aria-selected", "true");
}; };
/** /**
* Setup tab navigation * Setup tab navigation
*/ */
function setupTabNavigation() { function setupTabNavigation() {
document.querySelectorAll('.tab-btn').forEach(btn => { document.querySelectorAll(".tab-btn").forEach((btn) => {
btn.addEventListener('click', function() { btn.addEventListener("click", function () {
setActiveTab(this); setActiveTab(this);
}); });
}); });
@ -43,11 +47,13 @@
* Setup category navigation * Setup category navigation
*/ */
function setupCategoryNavigation() { function setupCategoryNavigation() {
document.addEventListener('click', function(e) { document.addEventListener("click", function (e) {
const categoryItem = e.target.closest('.category-item'); const categoryItem = e.target.closest(".category-item");
if (categoryItem) { if (categoryItem) {
document.querySelectorAll('.category-item').forEach(c => c.classList.remove('active')); document
categoryItem.classList.add('active'); .querySelectorAll(".category-item")
.forEach((c) => c.classList.remove("active"));
categoryItem.classList.add("active");
} }
}); });
} }
@ -56,20 +62,24 @@
* Setup view toggle (grid/list) * Setup view toggle (grid/list)
*/ */
function setupViewToggle() { function setupViewToggle() {
document.addEventListener('click', function(e) { document.addEventListener("click", function (e) {
const viewBtn = e.target.closest('.view-btn'); const viewBtn = e.target.closest(".view-btn");
if (viewBtn) { if (viewBtn) {
const controls = viewBtn.closest('.view-controls'); const controls = viewBtn.closest(".view-controls");
if (controls) { if (controls) {
controls.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active')); controls
viewBtn.classList.add('active'); .querySelectorAll(".view-btn")
.forEach((b) => b.classList.remove("active"));
viewBtn.classList.add("active");
const grid = document.querySelector('.prompts-grid, .templates-grid, .servers-grid, .models-grid, .news-grid'); const grid = document.querySelector(
".prompts-grid, .templates-grid, .servers-grid, .models-grid, .news-grid",
);
if (grid) { if (grid) {
if (viewBtn.title === 'List view') { if (viewBtn.title === "List view") {
grid.classList.add('list-view'); grid.classList.add("list-view");
} else { } else {
grid.classList.remove('list-view'); grid.classList.remove("list-view");
} }
} }
} }
@ -81,17 +91,22 @@
* Setup keyboard shortcuts * Setup keyboard shortcuts
*/ */
function setupKeyboardShortcuts() { function setupKeyboardShortcuts() {
document.addEventListener('keydown', function(e) { document.addEventListener("keydown", function (e) {
// Ctrl+K to focus search // Ctrl+K to focus search
if ((e.ctrlKey || e.metaKey) && e.key === 'k') { if ((e.ctrlKey || e.metaKey) && e.key === "k") {
e.preventDefault(); e.preventDefault();
const searchInput = document.querySelector('.search-box input'); const searchInput = document.querySelector(".search-box input");
if (searchInput) searchInput.focus(); if (searchInput) searchInput.focus();
} }
// Tab navigation with number keys // Tab navigation with number keys
if (!e.ctrlKey && !e.metaKey && !e.altKey && !e.target.matches('input, textarea')) { if (
const tabs = document.querySelectorAll('.tab-btn'); !e.ctrlKey &&
!e.metaKey &&
!e.altKey &&
!e.target.matches("input, textarea")
) {
const tabs = document.querySelectorAll(".tab-btn");
const num = parseInt(e.key); const num = parseInt(e.key);
if (num >= 1 && num <= tabs.length) { if (num >= 1 && num <= tabs.length) {
tabs[num - 1].click(); tabs[num - 1].click();
@ -99,7 +114,7 @@
} }
// Escape to close modals // Escape to close modals
if (e.key === 'Escape') { if (e.key === "Escape") {
closeModals(); closeModals();
} }
}); });
@ -109,10 +124,10 @@
* Setup HTMX events * Setup HTMX events
*/ */
function setupHTMXEvents() { function setupHTMXEvents() {
if (typeof htmx === 'undefined') return; if (typeof htmx === "undefined") return;
document.body.addEventListener('htmx:beforeRequest', function(e) { document.body.addEventListener("htmx:beforeRequest", function (e) {
if (e.detail.target && e.detail.target.id === 'content-area') { if (e.detail.target && e.detail.target.id === "content-area") {
e.detail.target.innerHTML = ` e.detail.target.innerHTML = `
<div class="loading-spinner"> <div class="loading-spinner">
<div class="spinner"></div> <div class="spinner"></div>
@ -122,11 +137,13 @@
} }
}); });
document.body.addEventListener('htmx:afterSwap', function(e) { document.body.addEventListener("htmx:afterSwap", function (e) {
// Re-initialize any dynamic content handlers after content swap // Re-initialize any dynamic content handlers after content swap
setupPromptCards(); setupPromptCards();
setupServerCards(); setupServerCards();
setupModelCards(); setupModelCards();
setupRepoCards();
setupAppCards();
}); });
} }
@ -134,10 +151,10 @@
* Setup prompt card interactions * Setup prompt card interactions
*/ */
function setupPromptCards() { function setupPromptCards() {
document.querySelectorAll('.prompt-card').forEach(card => { document.querySelectorAll(".prompt-card").forEach((card) => {
card.addEventListener('click', function(e) { card.addEventListener("click", function (e) {
// Don't trigger if clicking on action buttons // Don't trigger if clicking on action buttons
if (e.target.closest('.prompt-action-btn')) return; if (e.target.closest(".prompt-action-btn")) return;
const promptId = this.dataset.id; const promptId = this.dataset.id;
if (promptId) { if (promptId) {
@ -146,21 +163,21 @@
}); });
}); });
document.querySelectorAll('.prompt-action-btn').forEach(btn => { document.querySelectorAll(".prompt-action-btn").forEach((btn) => {
btn.addEventListener('click', function(e) { btn.addEventListener("click", function (e) {
e.stopPropagation(); e.stopPropagation();
const action = this.title.toLowerCase(); const action = this.title.toLowerCase();
const card = this.closest('.prompt-card'); const card = this.closest(".prompt-card");
const promptId = card?.dataset.id; const promptId = card?.dataset.id;
switch (action) { switch (action) {
case 'use': case "use":
usePrompt(promptId); usePrompt(promptId);
break; break;
case 'copy': case "copy":
copyPrompt(promptId); copyPrompt(promptId);
break; break;
case 'save': case "save":
savePrompt(promptId); savePrompt(promptId);
break; break;
} }
@ -172,8 +189,8 @@
* Setup server card interactions * Setup server card interactions
*/ */
function setupServerCards() { function setupServerCards() {
document.querySelectorAll('.server-card').forEach(card => { document.querySelectorAll(".server-card").forEach((card) => {
card.addEventListener('click', function() { card.addEventListener("click", function () {
const serverId = this.dataset.id; const serverId = this.dataset.id;
if (serverId) { if (serverId) {
showServerDetail(serverId); showServerDetail(serverId);
@ -186,8 +203,8 @@
* Setup model card interactions * Setup model card interactions
*/ */
function setupModelCards() { function setupModelCards() {
document.querySelectorAll('.model-card').forEach(card => { document.querySelectorAll(".model-card").forEach((card) => {
card.addEventListener('click', function() { card.addEventListener("click", function () {
const modelId = this.dataset.id; const modelId = this.dataset.id;
if (modelId) { if (modelId) {
showModelDetail(modelId); showModelDetail(modelId);
@ -200,12 +217,16 @@
* Show prompt detail modal/panel * Show prompt detail modal/panel
*/ */
function showPromptDetail(promptId) { function showPromptDetail(promptId) {
if (typeof htmx !== 'undefined') { if (typeof htmx !== "undefined") {
htmx.ajax('GET', `/api/sources/prompts/${promptId}`, { htmx
target: '#prompt-detail-panel', .ajax("GET", `/api/sources/prompts/${promptId}`, {
swap: 'innerHTML' target: "#prompt-detail-panel",
}).then(() => { swap: "innerHTML",
document.getElementById('prompt-detail-panel')?.classList.remove('hidden'); })
.then(() => {
document
.getElementById("prompt-detail-panel")
?.classList.remove("hidden");
}); });
} }
} }
@ -214,12 +235,14 @@
* Use a prompt * Use a prompt
*/ */
function usePrompt(promptId) { function usePrompt(promptId) {
if (typeof htmx !== 'undefined') { if (typeof htmx !== "undefined") {
htmx.ajax('POST', `/api/sources/prompts/${promptId}/use`, { htmx
swap: 'none' .ajax("POST", `/api/sources/prompts/${promptId}/use`, {
}).then(() => { swap: "none",
})
.then(() => {
// Navigate to the appropriate module // Navigate to the appropriate module
window.location.hash = '#research'; window.location.hash = "#research";
}); });
} }
} }
@ -228,13 +251,15 @@
* Copy prompt to clipboard * Copy prompt to clipboard
*/ */
function copyPrompt(promptId) { function copyPrompt(promptId) {
if (typeof htmx !== 'undefined') { if (typeof htmx !== "undefined") {
htmx.ajax('GET', `/api/sources/prompts/${promptId}/content`, { htmx
swap: 'none' .ajax("GET", `/api/sources/prompts/${promptId}/content`, {
}).then(response => { swap: "none",
})
.then((response) => {
// Parse response and copy to clipboard // Parse response and copy to clipboard
navigator.clipboard.writeText(response || ''); navigator.clipboard.writeText(response || "");
showToast('Prompt copied to clipboard'); showToast("Prompt copied to clipboard");
}); });
} }
} }
@ -243,29 +268,486 @@
* Save prompt to collection * Save prompt to collection
*/ */
function savePrompt(promptId) { function savePrompt(promptId) {
const collectionName = prompt('Enter collection name:'); const collectionName = prompt("Enter collection name:");
if (collectionName && typeof htmx !== 'undefined') { if (collectionName && typeof htmx !== "undefined") {
htmx.ajax('POST', '/api/sources/prompts/save', { htmx
.ajax("POST", "/api/sources/prompts/save", {
values: { values: {
promptId, promptId,
collection: collectionName collection: collectionName,
} },
}).then(() => { })
showToast('Prompt saved to collection'); .then(() => {
showToast("Prompt saved to collection");
}); });
} }
} }
/**
* Setup repository card interactions
*/
function setupRepoCards() {
document.querySelectorAll(".repo-card").forEach((card) => {
card.addEventListener("click", function (e) {
if (e.target.closest(".repo-action-btn")) return;
const repoId = this.dataset.id;
if (repoId) {
showRepoDetail(repoId);
}
});
});
document.querySelectorAll(".repo-action-btn").forEach((btn) => {
btn.addEventListener("click", function (e) {
e.stopPropagation();
const action = this.dataset.action;
const card = this.closest(".repo-card");
const repoId = card?.dataset.id;
const repoName = card?.dataset.name;
switch (action) {
case "connect":
connectRepo(repoId);
break;
case "disconnect":
disconnectRepo(repoId);
break;
case "mention":
insertMention("repo", repoName);
break;
case "browse":
browseRepo(repoId);
break;
}
});
});
}
/**
* Setup app card interactions
*/
function setupAppCards() {
document.querySelectorAll(".app-card").forEach((card) => {
card.addEventListener("click", function (e) {
if (e.target.closest(".app-action-btn")) return;
const appId = this.dataset.id;
if (appId) {
showAppDetail(appId);
}
});
});
document.querySelectorAll(".app-action-btn").forEach((btn) => {
btn.addEventListener("click", function (e) {
e.stopPropagation();
const action = this.dataset.action;
const card = this.closest(".app-card");
const appId = card?.dataset.id;
const appName = card?.dataset.name;
switch (action) {
case "open":
openApp(appId);
break;
case "edit":
editApp(appId);
break;
case "mention":
insertMention("app", appName);
break;
}
});
});
}
/**
* Setup @mention autocomplete for chat
*/
function setupMentionAutocomplete() {
// Listen for @ symbol in chat input
document.addEventListener("input", function (e) {
if (!e.target.matches(".chat-input, .message-input, #chat-input")) return;
const input = e.target;
const value = input.value;
const cursorPos = input.selectionStart;
// Find @ before cursor
const textBeforeCursor = value.substring(0, cursorPos);
const atMatch = textBeforeCursor.match(/@(\w*)$/);
if (atMatch) {
const query = atMatch[1];
showMentionSuggestions(input, query);
} else {
hideMentionSuggestions();
}
});
// Handle mention selection
document.addEventListener("click", function (e) {
const suggestion = e.target.closest(".mention-suggestion");
if (suggestion) {
const type = suggestion.dataset.type;
const name = suggestion.dataset.name;
applyMention(type, name);
}
});
// Keyboard navigation for suggestions
document.addEventListener("keydown", function (e) {
const suggestions = document.querySelector(".mention-suggestions");
if (!suggestions || suggestions.classList.contains("hidden")) return;
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
navigateMentionSuggestions(e.key === "ArrowDown" ? 1 : -1);
} else if (e.key === "Enter" || e.key === "Tab") {
const active = suggestions.querySelector(".mention-suggestion.active");
if (active) {
e.preventDefault();
active.click();
}
} else if (e.key === "Escape") {
hideMentionSuggestions();
}
});
}
/**
* Show mention suggestions dropdown
*/
function showMentionSuggestions(input, query) {
let suggestions = document.querySelector(".mention-suggestions");
if (!suggestions) {
suggestions = document.createElement("div");
suggestions.className = "mention-suggestions";
suggestions.style.cssText = `
position: absolute;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
max-height: 300px;
overflow-y: auto;
z-index: 1000;
`;
document.body.appendChild(suggestions);
}
// Fetch matching repos and apps
if (typeof htmx !== "undefined") {
htmx
.ajax("GET", `/api/sources/mentions?q=${encodeURIComponent(query)}`, {
swap: "none",
})
.then((response) => {
try {
const data = JSON.parse(response);
renderMentionSuggestions(suggestions, data, input);
} catch (e) {
// Fallback to showing cached data
renderMentionSuggestions(
suggestions,
getMockMentions(query),
input,
);
}
});
} else {
renderMentionSuggestions(suggestions, getMockMentions(query), input);
}
}
/**
* Get mock mentions for development
*/
function getMockMentions(query) {
const allMentions = [
{ type: "repo", name: "botserver", description: "Core API server" },
{ type: "repo", name: "botui", description: "Web UI components" },
{ type: "repo", name: "botbook", description: "Documentation" },
{ type: "repo", name: "bottemplates", description: "Bot templates" },
{ type: "app", name: "crm", description: "Customer management app" },
{ type: "app", name: "dashboard", description: "Analytics dashboard" },
{ type: "app", name: "myapp", description: "Custom application" },
];
if (!query) return allMentions.slice(0, 5);
return allMentions
.filter((m) => m.name.toLowerCase().includes(query.toLowerCase()))
.slice(0, 5);
}
/**
* Render mention suggestions
*/
function renderMentionSuggestions(container, data, input) {
if (!data || data.length === 0) {
container.classList.add("hidden");
return;
}
const rect = input.getBoundingClientRect();
container.style.top = `${rect.bottom + 4}px`;
container.style.left = `${rect.left}px`;
container.style.width = `${Math.min(rect.width, 320)}px`;
container.innerHTML = data
.map(
(item, index) => `
<div class="mention-suggestion ${index === 0 ? "active" : ""}"
data-type="${item.type}"
data-name="${item.name}">
<div class="mention-suggestion-icon ${item.type}">
${
item.type === "repo"
? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>'
: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>'
}
</div>
<div class="mention-suggestion-info">
<div class="mention-suggestion-name">@${item.name}</div>
<div class="mention-suggestion-type">${item.type === "repo" ? "Repository" : "App"} ${item.description || ""}</div>
</div>
</div>
`,
)
.join("");
container.classList.remove("hidden");
}
/**
* Hide mention suggestions
*/
function hideMentionSuggestions() {
const suggestions = document.querySelector(".mention-suggestions");
if (suggestions) {
suggestions.classList.add("hidden");
}
}
/**
* Navigate mention suggestions with keyboard
*/
function navigateMentionSuggestions(direction) {
const suggestions = document.querySelectorAll(".mention-suggestion");
const current = document.querySelector(".mention-suggestion.active");
let index = Array.from(suggestions).indexOf(current);
index += direction;
if (index < 0) index = suggestions.length - 1;
if (index >= suggestions.length) index = 0;
suggestions.forEach((s) => s.classList.remove("active"));
suggestions[index]?.classList.add("active");
suggestions[index]?.scrollIntoView({ block: "nearest" });
}
/**
* Apply selected mention to input
*/
function applyMention(type, name) {
const input = document.querySelector(
".chat-input, .message-input, #chat-input",
);
if (!input) return;
const value = input.value;
const cursorPos = input.selectionStart;
const textBeforeCursor = value.substring(0, cursorPos);
const textAfterCursor = value.substring(cursorPos);
// Replace @query with @name
const newTextBefore = textBeforeCursor.replace(/@\w*$/, `@${name} `);
input.value = newTextBefore + textAfterCursor;
input.selectionStart = input.selectionEnd = newTextBefore.length;
input.focus();
hideMentionSuggestions();
// Store context for the task
storeTaskContext(type, name);
}
/**
* Insert mention from Sources page
*/
function insertMention(type, name) {
// Navigate to chat and insert mention
const chatInput = document.querySelector(
".chat-input, .message-input, #chat-input",
);
if (chatInput) {
chatInput.value += `@${name} `;
chatInput.focus();
storeTaskContext(type, name);
showToast(`Added @${name} to chat context`);
} else {
// Store for next chat session
sessionStorage.setItem("pendingMention", JSON.stringify({ type, name }));
showToast(`@${name} will be added when you open chat`);
}
}
/**
* Store context for autonomous tasks
*/
function storeTaskContext(type, name) {
let context = JSON.parse(sessionStorage.getItem("taskContext") || "[]");
// Avoid duplicates
if (!context.find((c) => c.type === type && c.name === name)) {
context.push({ type, name, addedAt: Date.now() });
sessionStorage.setItem("taskContext", JSON.stringify(context));
}
}
/**
* Get current task context
*/
window.getTaskContext = function () {
return JSON.parse(sessionStorage.getItem("taskContext") || "[]");
};
/**
* Clear task context
*/
window.clearTaskContext = function () {
sessionStorage.removeItem("taskContext");
};
/**
* Show repository detail
*/
function showRepoDetail(repoId) {
if (typeof htmx !== "undefined") {
htmx
.ajax("GET", `/api/sources/repositories/${repoId}`, {
target: "#repo-detail-panel",
swap: "innerHTML",
})
.then(() => {
document
.getElementById("repo-detail-panel")
?.classList.remove("hidden");
});
}
}
/**
* Connect a repository
*/
function connectRepo(repoId) {
if (typeof htmx !== "undefined") {
htmx
.ajax("POST", `/api/sources/repositories/${repoId}/connect`, {
swap: "none",
})
.then(() => {
showToast("Repository connected");
// Refresh the repo card
htmx.ajax("GET", "/api/sources/repositories", {
target: "#content-area",
swap: "innerHTML",
});
});
}
}
/**
* Disconnect a repository
*/
function disconnectRepo(repoId) {
if (confirm("Disconnect this repository?")) {
if (typeof htmx !== "undefined") {
htmx
.ajax("DELETE", `/api/sources/repositories/${repoId}/connect`, {
swap: "none",
})
.then(() => {
showToast("Repository disconnected");
htmx.ajax("GET", "/api/sources/repositories", {
target: "#content-area",
swap: "innerHTML",
});
});
}
}
}
/**
* Browse repository files
*/
function browseRepo(repoId) {
if (typeof htmx !== "undefined") {
htmx
.ajax("GET", `/api/sources/repositories/${repoId}/files`, {
target: "#repo-browser-panel",
swap: "innerHTML",
})
.then(() => {
document
.getElementById("repo-browser-panel")
?.classList.remove("hidden");
});
}
}
/**
* Show app detail
*/
function showAppDetail(appId) {
if (typeof htmx !== "undefined") {
htmx
.ajax("GET", `/api/sources/apps/${appId}`, {
target: "#app-detail-panel",
swap: "innerHTML",
})
.then(() => {
document
.getElementById("app-detail-panel")
?.classList.remove("hidden");
});
}
}
/**
* Open an app in new tab
*/
function openApp(appId) {
window.open(`/apps/${appId}`, "_blank");
}
/**
* Edit an app (opens in Tasks with context)
*/
function editApp(appId) {
// Store app context and navigate to tasks
storeTaskContext("app", appId);
window.location.hash = "#tasks";
showToast(`Editing @${appId} - describe your changes`);
}
/** /**
* Show server detail * Show server detail
*/ */
function showServerDetail(serverId) { function showServerDetail(serverId) {
if (typeof htmx !== 'undefined') { if (typeof htmx !== "undefined") {
htmx.ajax('GET', `/api/sources/mcp-servers/${serverId}`, { htmx
target: '#server-detail-panel', .ajax("GET", `/api/sources/mcp-servers/${serverId}`, {
swap: 'innerHTML' target: "#server-detail-panel",
}).then(() => { swap: "innerHTML",
document.getElementById('server-detail-panel')?.classList.remove('hidden'); })
.then(() => {
document
.getElementById("server-detail-panel")
?.classList.remove("hidden");
}); });
} }
} }
@ -274,12 +756,16 @@
* Show model detail * Show model detail
*/ */
function showModelDetail(modelId) { function showModelDetail(modelId) {
if (typeof htmx !== 'undefined') { if (typeof htmx !== "undefined") {
htmx.ajax('GET', `/api/sources/models/${modelId}`, { htmx
target: '#model-detail-panel', .ajax("GET", `/api/sources/models/${modelId}`, {
swap: 'innerHTML' target: "#model-detail-panel",
}).then(() => { swap: "innerHTML",
document.getElementById('model-detail-panel')?.classList.remove('hidden'); })
.then(() => {
document
.getElementById("model-detail-panel")
?.classList.remove("hidden");
}); });
} }
} }
@ -288,35 +774,35 @@
* Close all modals * Close all modals
*/ */
function closeModals() { function closeModals() {
document.querySelectorAll('.modal, .detail-panel').forEach(modal => { document.querySelectorAll(".modal, .detail-panel").forEach((modal) => {
modal.classList.add('hidden'); modal.classList.add("hidden");
}); });
} }
/** /**
* Show toast notification * Show toast notification
*/ */
function showToast(message, type = 'success') { function showToast(message, type = "success") {
const toast = document.createElement('div'); const toast = document.createElement("div");
toast.className = `toast toast-${type}`; toast.className = `toast toast-${type}`;
toast.textContent = message; toast.textContent = message;
document.body.appendChild(toast); document.body.appendChild(toast);
// Trigger animation // Trigger animation
requestAnimationFrame(() => { requestAnimationFrame(() => {
toast.classList.add('show'); toast.classList.add("show");
}); });
// Remove after delay // Remove after delay
setTimeout(() => { setTimeout(() => {
toast.classList.remove('show'); toast.classList.remove("show");
setTimeout(() => toast.remove(), 300); setTimeout(() => toast.remove(), 300);
}, 3000); }, 3000);
} }
// Initialize on DOM ready // Initialize on DOM ready
if (document.readyState === 'loading') { if (document.readyState === "loading") {
document.addEventListener('DOMContentLoaded', init); document.addEventListener("DOMContentLoaded", init);
} else { } else {
init(); init();
} }
@ -328,6 +814,16 @@
usePrompt, usePrompt,
copyPrompt, copyPrompt,
savePrompt, savePrompt,
showToast showToast,
showRepoDetail,
connectRepo,
disconnectRepo,
browseRepo,
showAppDetail,
openApp,
editApp,
insertMention,
getTaskContext: window.getTaskContext,
clearTaskContext: window.clearTaskContext,
}; };
})(); })();