Files
oikos/web/src/App.svelte
dtoro e8e230b4a5
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
nomos+web: streaming, provider routing, event gap-fill, embedded UI; fix approval FK & session context
Agent (cmd/nomos):
- Stream LLM tokens via NewStreaming; emit text_delta then final text.
- OpenRouter provider routing: data_collection=deny (ZDR) + require_parameters;
  NOMOS_PROVIDER_SORT opt-in; Exacto via model suffix.
- Multi-turn: reload session history into context; UI passes session id.
- Fix agent_activity logging (agent_id/session_id) and mcpClient data race.

Events (live control-room feed):
- approval.created (mcp), approval.decided (api), execution.completed/failed
  (approved-action path), signal.raised/resolved + health.changed (scheduler,
  transition-gated).

Fixes:
- createApproval FK violation (reuse execution entity) — the agent's only
  write path; log the previously-swallowed errors.

Web UI:
- Embed web/dist via //go:embed (single binary); Dockerfile builds SPA into
  the Go stage; committed .gitkeep placeholder keeps backend-only builds green.
- Caddy: Authentik-gated /agent/* -> nomos so the UI reaches the agent
  same-origin in production.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 15:22:27 +02:00

156 lines
3.1 KiB
Svelte

<script lang="ts">
import Chat from '$lib/../pages/Chat.svelte'
import Sessions from '$lib/../pages/Sessions.svelte'
import { newChat } from '$lib/stores/chat'
import { onMount } from 'svelte'
import { slide } from 'svelte/transition'
let page = $state('chat')
let drawerOpen = $state(false)
onMount(() => {
function sync() {
page = location.hash.slice(2) || 'chat'
}
sync()
window.addEventListener('hashchange', sync)
return () => window.removeEventListener('hashchange', sync)
})
function navigate(p: string) {
location.hash = '#/' + p
}
</script>
<div class="app">
<nav class="sidebar">
<div class="logo">Oikos</div>
<button class="nav-btn" onclick={() => { newChat(); navigate('chat') }}>
<span class="nav-icon"></span>
<span>New</span>
</button>
<button class="nav-btn" class:active={page === 'chat'} onclick={() => navigate('chat')}>
<span class="nav-icon">💬</span>
<span>Chat</span>
</button>
<button class="nav-btn" class:active={page === 'sessions'} onclick={() => navigate('sessions')}>
<span class="nav-icon">📋</span>
<span>Sessions</span>
</button>
<div class="spacer"></div>
<button class="drawer-toggle" onclick={() => drawerOpen = !drawerOpen}>
Chat {drawerOpen ? '▼' : '▲'}
</button>
</nav>
<main class="main">
{#if page === 'chat'}
<Chat />
{:else if page === 'sessions'}
<Sessions />
{:else}
<Chat />
{/if}
</main>
{#if drawerOpen}
<aside class="drawer" transition:slide={{ axis: 'x' }}>
<Chat />
</aside>
{/if}
</div>
<style>
.app {
display: flex;
height: 100vh;
overflow: hidden;
}
.sidebar {
width: 56px;
background: var(--bg-surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
align-items: center;
padding: 0.75rem 0;
gap: 0.5rem;
}
.logo {
font-size: 1.25rem;
font-weight: 700;
color: var(--accent-blue);
margin-bottom: 0.5rem;
user-select: none;
}
.nav-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
padding: 0.5rem;
border: none;
background: none;
color: var(--text-muted);
font-family: inherit;
font-size: 0.625rem;
cursor: pointer;
border-radius: 6px;
width: 44px;
transition: background 0.15s, color 0.15s;
}
.nav-btn:hover {
background: var(--bg-hover);
color: var(--text);
}
.nav-btn.active {
background: var(--bg-active);
color: var(--accent-blue);
}
.nav-icon {
font-size: 1rem;
}
.spacer {
flex: 1;
}
.drawer-toggle {
border: none;
background: none;
color: var(--text-muted);
font-family: inherit;
font-size: 0.625rem;
cursor: pointer;
padding: 0.5rem;
}
.drawer-toggle:hover {
color: var(--text);
}
.main {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.drawer {
width: 380px;
border-left: 1px solid var(--border);
background: var(--bg);
overflow: hidden;
display: flex;
flex-direction: column;
}
</style>