Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
{
"mcpServers": {
"vibecockpit": {
"command": "vibecockpit",
"args": ["--mcp"]
"args": [
"--mcp"
],
"command": "vibecockpit"
}
}
}
44 changes: 43 additions & 1 deletion frontend/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@
import BoardView from "./components/BoardView.svelte";
import AgentMonitor from "./components/AgentMonitor.svelte";
import Scheduler from "./components/Scheduler.svelte";
import ChatView from "./components/ChatView.svelte";

// ─── Reactive State (Svelte 5 runes) ───

const validPages = ["dashboard", "planner", "agents", "scheduler", "sessions", "costs", "inventory", "settings"];
const validPages = ["dashboard", "planner", "agents", "scheduler", "chat", "sessions", "costs", "inventory", "settings"];
const redirects = { stats: "dashboard", security: "inventory", mcp: "settings", boards: "planner" };
function pageFromPath() {
const p = window.location.pathname.replace(/^\/+/, "").split("/")[0];
Expand Down Expand Up @@ -127,6 +128,7 @@
unsubGroup();
unsubActiveFilters();
stopAutoRefresh();
if (notifTimer) clearInterval(notifTimer);
window.removeEventListener("popstate", handlePopState);
});

Expand Down Expand Up @@ -163,6 +165,24 @@
page = pageFromPath();
}

let notifTimer;

function startNotificationPolling() {
notifTimer = setInterval(async () => {
try {
const r = await fetch("/api/notifications");
if (!r.ok) return;
const notifs = await r.json();
for (const n of notifs) {
showToast(n.message, n.type === "completed" ? "success" : "error");
if (Notification.permission === "granted") {
new Notification(n.title, { body: n.message });
}
}
} catch { /* ignore */ }
}, 5000);
}

onMount(async () => {
window.addEventListener("popstate", handlePopState);
await loadConfig();
Expand All @@ -174,6 +194,8 @@
}
await refresh(false);
startAutoRefresh();
if (Notification.permission === "default") Notification.requestPermission();
startNotificationPolling();
});

function wizardToggle(id) {
Expand Down Expand Up @@ -731,6 +753,10 @@
<svg class="sidebar-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
<span class="sidebar-label">Scheduler</span>
</button>
<button class="sidebar-btn" class:active={page === "chat"} onclick={() => navigateTo("chat")} title="Chat with AI agent">
<svg class="sidebar-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 11.5a8.38 8.38 0 01-.9 3.8 8.5 8.5 0 01-7.6 4.7 8.38 8.38 0 01-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 01-.9-3.8 8.5 8.5 0 014.7-7.6 8.38 8.38 0 013.8-.9h.5a8.48 8.48 0 018 8v.5z"/></svg>
<span class="sidebar-label">Chat</span>
</button>
<button class="sidebar-btn" class:active={page === "sessions"} onclick={() => navigateTo("sessions")}>
<svg class="sidebar-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>
<span class="sidebar-label">Sessions</span>
Expand Down Expand Up @@ -811,6 +837,10 @@
/>
</main>
{:else if page === "sessions"}
<div class="page-bar">
<h1 class="page-bar-title">Sessions</h1>
<span class="page-bar-subtitle">{sessionList.length} sessions across all tools</span>
</div>
<!-- Search Bar -->
<div class="search-wrap">
<div class="search-box">
Expand Down Expand Up @@ -1098,9 +1128,21 @@
<AgentMonitor onnavigate={navigateTo} />
</main>
{:else if page === "scheduler"}
<div class="page-bar">
<h1 class="page-bar-title">Scheduler</h1>
<span class="page-bar-subtitle">Cron-based agent jobs</span>
</div>
<main>
<Scheduler />
</main>
{:else if page === "chat"}
<div class="page-bar">
<h1 class="page-bar-title">Chat</h1>
<span class="page-bar-subtitle">Conversational AI with MCP tools</span>
</div>
<main class="chat-main-container">
<ChatView />
</main>
{:else if page === "costs"}
<div class="page-bar">
<h1 class="page-bar-title">Costs</h1>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ code{

/* ─── Main ─── */
main{flex:1;padding:1.5rem 2rem 3rem;animation:pageIn .2s ease}
main.chat-main-container{padding:0;overflow:hidden}
@keyframes pageIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}
.empty{text-align:center;padding:4rem 1rem;color:var(--text-secondary)}
.empty h3{font-size:1.1rem;color:var(--text);margin-bottom:.5rem}
Expand Down
68 changes: 64 additions & 4 deletions frontend/src/components/AgentMonitor.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script>
import { onMount, onDestroy } from "svelte";
import { relativeTime } from "../lib/utils.js";
import { quickRun, fetchSessions } from "../lib/api.js";
import { quickRun, fetchSessions, createJob } from "../lib/api.js";

let { onnavigate } = $props();
let diffContent = $state("");
Expand All @@ -16,8 +16,10 @@
let searchQuery = $state("");
let filterSource = $state("all");
let filterStatus = $state("all");
let currentPage = $state(0);
const pageSize = 15;
let showQuickRun = $state(false);
let quickForm = $state({ prompt: "", project: "", tool: "claude", model: "" });
let quickForm = $state({ prompt: "", project: "", tool: "claude", model: "", saveAsJob: false, jobName: "", jobCron: "0 9 * * *" });
let knownProjects = $state([]);
let modelsByProvider = $state({});
let allModels = $state([]);
Expand Down Expand Up @@ -53,8 +55,20 @@
async function dispatchQuickRun() {
try {
await quickRun(quickForm.prompt, quickForm.project, quickForm.tool, quickForm.model);
if (quickForm.saveAsJob && quickForm.jobName) {
await createJob({
name: quickForm.jobName,
cron: quickForm.jobCron,
tool: quickForm.tool,
model: quickForm.model,
prompt: quickForm.prompt,
project: quickForm.project,
mcpServers: ["vibecockpit"],
enabled: true,
});
}
showQuickRun = false;
quickForm = { prompt: "", project: "", tool: "claude", model: "" };
quickForm = { prompt: "", project: "", tool: "claude", model: "", saveAsJob: false, jobName: "", jobCron: "0 9 * * *" };
setTimeout(loadAgents, 1000);
} catch (e) {
alert("Error: " + e.message);
Expand Down Expand Up @@ -89,6 +103,9 @@
return list;
});

let totalPages = $derived(Math.ceil(filteredAgents().length / pageSize));
let pagedAgents = $derived(filteredAgents().slice(currentPage * pageSize, (currentPage + 1) * pageSize));

async function loadAgents() {
try {
const r = await fetch("/api/agents");
Expand Down Expand Up @@ -265,7 +282,7 @@
<div class="agent-no-match">No matching runs</div>
{/if}

{#each filteredAgents() as run (run.taskId)}
{#each pagedAgents as run (run.taskId)}
<button class="agent-item" class:agent-item-active={selectedAgent?.taskId === run.taskId} onclick={() => selectAgent(run)}>
<div class="agent-item-status">
{#if run.status === "running"}
Expand Down Expand Up @@ -299,6 +316,14 @@
</div>
</button>
{/each}

{#if totalPages > 1}
<div class="agent-pagination">
<button class="agent-page-btn" onclick={() => { currentPage = Math.max(0, currentPage - 1); }} disabled={currentPage === 0}>&laquo;</button>
<span class="agent-page-info">{currentPage + 1} / {totalPages}</span>
<button class="agent-page-btn" onclick={() => { currentPage = Math.min(totalPages - 1, currentPage + 1); }} disabled={currentPage >= totalPages - 1}>&raquo;</button>
</div>
{/if}
</div>

<!-- Agent detail -->
Expand Down Expand Up @@ -527,6 +552,25 @@
</div>
</div>

<div class="qr-save-job">
<label class="qr-save-label">
<input type="checkbox" bind:checked={quickForm.saveAsJob} />
<span>Also save as scheduled job</span>
</label>
{#if quickForm.saveAsJob}
<div class="qr-row" style="margin-top:.4rem">
<div class="qr-field">
<label for="qr-jobname">Job name</label>
<input id="qr-jobname" type="text" bind:value={quickForm.jobName} placeholder="e.g. Daily Report" />
</div>
<div class="qr-field">
<label for="qr-jobcron">Schedule</label>
<input id="qr-jobcron" type="text" bind:value={quickForm.jobCron} placeholder="0 9 * * *" />
</div>
</div>
{/if}
</div>

<div class="qr-actions">
<button class="btn btn-sm" onclick={() => { showQuickRun = false; }}>Cancel</button>
<button class="btn btn-sm btn-primary" onclick={dispatchQuickRun} disabled={!quickForm.prompt.trim()}>Run</button>
Expand Down Expand Up @@ -584,6 +628,14 @@

.agent-no-match { text-align: center; padding: 1.5rem .5rem; font-size: .82rem; color: var(--text-muted); }

.agent-pagination { display: flex; align-items: center; justify-content: center; gap: .4rem; margin-top: .4rem; }
.agent-page-btn {
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm);
padding: .2rem .5rem; font-size: .75rem; cursor: pointer; color: var(--text);
}
.agent-page-btn:disabled { opacity: .4; cursor: not-allowed; }
.agent-page-info { font-size: .72rem; color: var(--text-muted); }

.agent-item { display: flex; align-items: center; gap: .5rem; width: 100%; padding: .5rem .6rem;
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm);
cursor: pointer; font-family: inherit; color: var(--text); text-align: left; margin-bottom: .3rem; transition: border-color .15s; }
Expand Down Expand Up @@ -696,6 +748,14 @@
.qr-field textarea:focus, .qr-field select:focus { outline: none; border-color: var(--primary); }
.qr-row { display: flex; gap: .5rem; }
.qr-row .qr-field { flex: 1; }
.qr-save-job {
margin-top: .6rem; padding: .5rem .6rem; border: 1px solid var(--border);
border-radius: var(--radius-sm); background: var(--bg);
}
.qr-save-label {
display: flex; align-items: center; gap: .4rem; font-size: .78rem; cursor: pointer;
}
.qr-save-label input { cursor: pointer; }
.qr-actions { display: flex; gap: .3rem; justify-content: flex-end; margin-top: .8rem; }

.agent-detail-stat-wide { flex-basis: 100%; }
Expand Down
Loading
Loading