diff --git a/.mcp.json b/.mcp.json index 87f9ad1..0a6328c 100644 --- a/.mcp.json +++ b/.mcp.json @@ -1,8 +1,10 @@ { "mcpServers": { "vibecockpit": { - "command": "vibecockpit", - "args": ["--mcp"] + "args": [ + "--mcp" + ], + "command": "vibecockpit" } } } diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 27bcc74..f53858b 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -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]; @@ -127,6 +128,7 @@ unsubGroup(); unsubActiveFilters(); stopAutoRefresh(); + if (notifTimer) clearInterval(notifTimer); window.removeEventListener("popstate", handlePopState); }); @@ -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(); @@ -174,6 +194,8 @@ } await refresh(false); startAutoRefresh(); + if (Notification.permission === "default") Notification.requestPermission(); + startNotificationPolling(); }); function wizardToggle(id) { @@ -731,6 +753,10 @@ Scheduler + + + {#if chats.length === 0 && !showNewChat} +
No chats yet
+ {/if} + + {#each chats as c (c.id)} + + {/each} + + +
+ {#if showNewChat} +
+

New Chat

+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + + Comma-separated. vibecockpit is always included. +
+
+ + +
+
+ {:else if activeChat} +
+
+ {activeChat.name} + {activeChat.tool}{activeChat.model ? " / " + activeChat.model : ""}{activeChat.project ? " · " + activeChat.project.split('/').pop() : ""} +
+
+ {#if chatFiles.length > 0} + + {/if} + +
+
+ + {#if showFiles && chatFiles.length > 0} +
+ {#if viewingFile} +
+
+ {viewingFile.name} + +
+
{viewingFile.content}
+
+ {:else} +
+ {#each chatFiles as f (f.name)} + + {/each} +
+ {/if} +
+ {/if} + +
+ {#if activeChat.messages.length === 0} +
Start the conversation. The agent has access to VibeCockpit MCP — ask it to create tasks, schedule jobs, check costs, etc.
+ {/if} + {#each activeChat.messages as msg, i (i)} +
+
+ {msg.role === 'user' ? 'You' : (msg.tool || activeChat.tool)} + {#if msg.model && msg.role === 'assistant'} + {msg.model} + {/if} +
+ {#if msg.role === "assistant"} +
{@html renderMarkdown(msg.content)}
+ {:else} +
{msg.content}
+ {/if} +
+ {/each} + {#if sending} +
+
{activeChat.tool}
+
Thinking...
+
+ {/if} +
+
+ +
+
+ + +
+
+ + + Override per message +
+
+ {:else} +
+

Select a chat or create a new one.

+

Chats have VibeCockpit MCP — ask the agent to create tickets, schedule jobs, check session costs, or work on files in a project.

+
+ {/if} +
+ + + diff --git a/frontend/src/components/CostsDashboard.svelte b/frontend/src/components/CostsDashboard.svelte index dbdd898..34022a4 100644 --- a/frontend/src/components/CostsDashboard.svelte +++ b/frontend/src/components/CostsDashboard.svelte @@ -1,8 +1,29 @@
-
-
-

Scheduler

- Run agents on a schedule — {jobs.length} job{jobs.length !== 1 ? 's' : ''} -
+
+ {jobs.length} job{jobs.length !== 1 ? 's' : ''}
@@ -223,8 +222,10 @@
{:else} + {@const totalJobPages = Math.ceil(jobs.length / jobPageSize)} + {@const pagedJobs = jobs.slice(jobPage * jobPageSize, (jobPage + 1) * jobPageSize)}
- {#each jobs as job (job.id)} + {#each pagedJobs as job (job.id)}
@@ -282,7 +283,11 @@
{/if} -
{job.prompt.length > 140 ? job.prompt.slice(0, 140) + '...' : job.prompt}
+ {#if job.lastOutput} +
{job.lastOutput}
+ {:else} +
{job.prompt.length > 140 ? job.prompt.slice(0, 140) + '...' : job.prompt}
+ {/if}
{/each} + + {#if totalJobPages > 1} +
+ + {jobPage + 1} / {totalJobPages} + +
+ {/if}
{/if}
@@ -484,18 +497,25 @@