Skip to content

Commit 3523ec4

Browse files
feat: Add Observability dashboard and genericize agent identity
- Add full Observability system (server + Vue client) - Genericize all agent references to use DA env var - Rename initialize-pai-session.ts to initialize-session.ts - Add env var documentation to settings.json - Server uses process.env.DA || 'main' for agent identification - Client shows "User" for prompts, DA name for agent work 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 5555748 commit 3523ec4

86 files changed

Lines changed: 14953 additions & 7 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/Observability/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules/
2+
.vite/
3+
dist/
4+
*.log
5+
.DS_Store
6+
.tmp/
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Vue 3 + TypeScript + Vite
2+
3+
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
4+
5+
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

.claude/Observability/apps/client/bun.lock

Lines changed: 455 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/binoculars.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Agents</title>
8+
</head>
9+
<body>
10+
<div id="app"></div>
11+
<script type="module" src="/src/main.ts"></script>
12+
</body>
13+
</html>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "multi-agent-observability-client",
3+
"private": true,
4+
"version": "1.2.0",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "vue-tsc -b && vite build",
9+
"preview": "vite preview"
10+
},
11+
"dependencies": {
12+
"lucide-vue-next": "^0.548.0",
13+
"vue": "^3.5.17"
14+
},
15+
"devDependencies": {
16+
"@types/node": "^22.11.2",
17+
"@vitejs/plugin-vue": "^6.0.0",
18+
"@vue/tsconfig": "^0.7.0",
19+
"autoprefixer": "^10.4.20",
20+
"postcss": "^8.5.3",
21+
"tailwindcss": "^3.4.16",
22+
"typescript": "~5.8.3",
23+
"vite": "^7.0.4",
24+
"vue-tsc": "^2.2.12"
25+
}
26+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default {
2+
plugins: {
3+
tailwindcss: {},
4+
autoprefixer: {},
5+
},
6+
}
Lines changed: 16 additions & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
<template>
2+
<div class="compact-mode h-screen flex flex-col theme-tokyo-night dashboard-modern">
3+
<!-- Background gradient overlay -->
4+
<div class="fixed inset-0 bg-gradient-to-br from-[#1a1b26] via-[#16161e] to-[#0f0f14] -z-10"></div>
5+
<div class="fixed inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxnIGZpbGw9IiM3YWEyZjciIGZpbGwtb3BhY2l0eT0iMC4wMiI+PGNpcmNsZSBjeD0iMzAiIGN5PSIzMCIgcj0iMSIvPjwvZz48L2c+PC9zdmc+')] -z-10 opacity-50"></div>
6+
7+
<!-- Filters -->
8+
<FilterPanel
9+
v-if="showFilters"
10+
class="short:hidden glass-panel-elevated mx-4 mt-4 rounded-2xl"
11+
:filters="filters"
12+
@update:filters="filters = $event"
13+
/>
14+
15+
<!-- Live Pulse Chart - Glass Panel -->
16+
<div class="mx-4 mt-4 mobile:mx-2 mobile:mt-2">
17+
<div class="glass-panel rounded-2xl overflow-hidden">
18+
<LivePulseChart
19+
:events="events"
20+
:filters="filters"
21+
:time-range="currentTimeRange"
22+
@update-unique-apps="uniqueAppNames = $event"
23+
@update-all-apps="allAppNames = $event"
24+
@update-time-range="currentTimeRange = $event"
25+
@update-heat-level="heatLevel = $event"
26+
@update-events-per-minute="eventsPerMinute = $event"
27+
@update-agent-counts="agentCounts = $event"
28+
@clear-events="handleClearClick"
29+
@toggle-filters="showFilters = !showFilters"
30+
/>
31+
</div>
32+
</div>
33+
34+
<!-- Agent Swim Lane Container - Glass Panel -->
35+
<div v-if="selectedAgentLanes.length > 0" class="mx-4 mt-4 mobile:mx-2 mobile:mt-2">
36+
<div class="glass-panel rounded-2xl p-4 mobile:p-2">
37+
<AgentSwimLaneContainer
38+
:selected-agents="selectedAgentLanes"
39+
:events="events"
40+
:time-range="currentTimeRange"
41+
@update:selected-agents="selectedAgentLanes = $event"
42+
/>
43+
</div>
44+
</div>
45+
46+
<!-- Timeline - Glass Panel -->
47+
<div class="flex flex-col flex-1 overflow-hidden mx-4 my-4 mobile:mx-2 mobile:my-2">
48+
<div class="glass-panel rounded-2xl flex-1 overflow-hidden">
49+
<EventTimeline
50+
:events="events"
51+
:filters="filters"
52+
:unique-app-names="uniqueAppNames"
53+
:all-app-names="allAppNames"
54+
:heat-level="heatLevel"
55+
:agent-counts="agentCounts"
56+
:events-per-minute="eventsPerMinute"
57+
:time-range="currentTimeRange"
58+
:time-ranges="['1M', '2M', '4M', '8M', '16M']"
59+
v-model:stick-to-bottom="stickToBottom"
60+
@select-agent="toggleAgentLane"
61+
@set-time-range="setTimeRangeFromTimeline"
62+
/>
63+
</div>
64+
</div>
65+
66+
<!-- Stick to bottom button - Glass styling -->
67+
<StickScrollButton
68+
class="short:hidden glass-button"
69+
:stick-to-bottom="stickToBottom"
70+
@toggle="stickToBottom = !stickToBottom"
71+
/>
72+
73+
<!-- Error message - Glass styling -->
74+
<div
75+
v-if="error"
76+
class="fixed bottom-6 left-6 mobile:bottom-4 mobile:left-4 mobile:right-4 glass-panel-elevated rounded-xl px-4 py-3 mobile:px-3 mobile:py-2 border-l-4 border-[var(--theme-accent-error)]"
77+
>
78+
<span class="text-[var(--theme-accent-error)] font-medium">{{ error }}</span>
79+
</div>
80+
81+
<!-- Theme Manager -->
82+
<ThemeManager
83+
:is-open="showThemeManager"
84+
@close="showThemeManager = false"
85+
/>
86+
87+
<!-- Toast Notifications -->
88+
<ToastNotification
89+
v-for="(toast, index) in toasts"
90+
:key="toast.id"
91+
:index="index"
92+
:agent-name="toast.agentName"
93+
:agent-color="toast.agentColor"
94+
@dismiss="dismissToast(toast.id)"
95+
/>
96+
</div>
97+
</template>
98+
99+
<script setup lang="ts">
100+
import { ref, watch } from 'vue';
101+
import type { TimeRange } from './types';
102+
import { useWebSocket } from './composables/useWebSocket';
103+
import { useThemes } from './composables/useThemes';
104+
import { useEventColors } from './composables/useEventColors';
105+
import EventTimeline from './components/EventTimeline.vue';
106+
import FilterPanel from './components/FilterPanel.vue';
107+
import StickScrollButton from './components/StickScrollButton.vue';
108+
import LivePulseChart from './components/LivePulseChart.vue';
109+
import ThemeManager from './components/ThemeManager.vue';
110+
import ToastNotification from './components/ToastNotification.vue';
111+
import AgentSwimLaneContainer from './components/AgentSwimLaneContainer.vue';
112+
113+
// WebSocket connection
114+
const { events, isConnected, error, clearEvents } = useWebSocket('ws://localhost:4000/stream');
115+
116+
// Theme management (sets up theme system)
117+
useThemes();
118+
119+
// Event colors
120+
const { getHexColorForApp } = useEventColors();
121+
122+
// Filters
123+
const filters = ref({
124+
sourceApp: '',
125+
sessionId: '',
126+
eventType: ''
127+
});
128+
129+
// UI state
130+
const stickToBottom = ref(true);
131+
const showThemeManager = ref(false);
132+
const showFilters = ref(false);
133+
const uniqueAppNames = ref<string[]>([]); // Apps active in current time window
134+
const allAppNames = ref<string[]>([]); // All apps ever seen in session
135+
const selectedAgentLanes = ref<string[]>([]);
136+
const currentTimeRange = ref<TimeRange>('1M'); // Current time range from LivePulseChart
137+
const heatLevel = ref({ intensity: 0, color: '#565f89', label: 'Low' });
138+
const agentCounts = ref<Record<string, number>>({});
139+
const eventsPerMinute = ref(0);
140+
141+
// Toast notifications
142+
interface Toast {
143+
id: number;
144+
agentName: string;
145+
agentColor: string;
146+
}
147+
const toasts = ref<Toast[]>([]);
148+
let toastIdCounter = 0;
149+
const seenAgents = new Set<string>();
150+
151+
// Watch for new agents and show toast
152+
watch(uniqueAppNames, (newAppNames) => {
153+
// Find agents that are new (not in seenAgents set)
154+
newAppNames.forEach(appName => {
155+
if (!seenAgents.has(appName)) {
156+
seenAgents.add(appName);
157+
// Show toast for new agent
158+
const toast: Toast = {
159+
id: toastIdCounter++,
160+
agentName: appName,
161+
agentColor: getHexColorForApp(appName)
162+
};
163+
toasts.value.push(toast);
164+
}
165+
});
166+
}, { deep: true });
167+
168+
const dismissToast = (id: number) => {
169+
const index = toasts.value.findIndex(t => t.id === id);
170+
if (index !== -1) {
171+
toasts.value.splice(index, 1);
172+
}
173+
};
174+
175+
// Handle agent tag clicks for swim lanes
176+
const toggleAgentLane = (agentName: string) => {
177+
const index = selectedAgentLanes.value.indexOf(agentName);
178+
if (index >= 0) {
179+
// Remove from comparison
180+
selectedAgentLanes.value.splice(index, 1);
181+
} else {
182+
// Add to comparison
183+
selectedAgentLanes.value.push(agentName);
184+
}
185+
};
186+
187+
// Handle clear button click
188+
const handleClearClick = () => {
189+
clearEvents();
190+
selectedAgentLanes.value = [];
191+
};
192+
193+
// Handle time range change from EventTimeline
194+
const setTimeRangeFromTimeline = (range: TimeRange) => {
195+
currentTimeRange.value = range;
196+
};
197+
198+
// Debug handler for theme manager
199+
const handleThemeManagerClick = () => {
200+
console.log('Theme manager button clicked!');
201+
showThemeManager.value = true;
202+
};
203+
</script>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
@font-face {
2+
font-family: 'concourse-t3';
3+
src: url('./fonts/concourse_t3_regular-webfont.woff') format('woff');
4+
font-style: normal;
5+
font-weight: normal;
6+
font-stretch: normal;
7+
font-display: swap;
8+
}
9+
10+
@font-face {
11+
font-family: 'concourse-c3';
12+
src: url('./fonts/concourse_c3_regular.woff') format('woff');
13+
font-style: normal;
14+
font-weight: normal;
15+
font-stretch: normal;
16+
font-display: swap;
17+
}
18+
19+
@font-face {
20+
font-family: 'equity-text-b';
21+
src: url('./fonts/equity_text_b_regular-webfont.woff') format('woff');
22+
font-style: normal;
23+
font-weight: normal;
24+
font-stretch: normal;
25+
font-display: swap;
26+
}
27+
28+
@font-face {
29+
font-family: 'advocate';
30+
font-style: normal;
31+
font-weight: normal;
32+
font-stretch: normal;
33+
font-display: swap;
34+
src: url('./fonts/advocate_14_cond_reg.woff2') format('woff2');
35+
}
36+
37+
/* Valkyrie text fonts */
38+
@font-face {
39+
font-family: 'valkyrie-text';
40+
src: url('./fonts/valkyrie_a_regular.woff2') format('woff2');
41+
font-weight: normal;
42+
font-style: normal;
43+
font-display: swap;
44+
}
45+
46+
@font-face {
47+
font-family: 'valkyrie-text';
48+
src: url('./fonts/valkyrie_a_bold.woff2') format('woff2');
49+
font-weight: bold;
50+
font-style: normal;
51+
font-display: swap;
52+
}
53+
54+
@font-face {
55+
font-family: 'valkyrie-text';
56+
src: url('./fonts/valkyrie_a_italic.woff2') format('woff2');
57+
font-weight: normal;
58+
font-style: italic;
59+
font-display: swap;
60+
}
61+
62+
@font-face {
63+
font-family: 'valkyrie-text';
64+
src: url('./fonts/valkyrie_a_bold_italic.woff2') format('woff2');
65+
font-weight: bold;
66+
font-style: italic;
67+
font-display: swap;
68+
}

0 commit comments

Comments
 (0)