Skip to content

Commit ab63cbd

Browse files
wesmclaude
andauthored
feat: add AI-powered Insights with multi-agent generation (#16)
## Summary - **Insights feature** for AI-generated analysis of agent sessions, supporting Claude, Codex, and Gemini as generation backends - Full-stack: `insights` SQLite table with FTS5, Go HTTP handlers (`GET/DELETE /api/v1/insights/{id}`, `POST /api/v1/insights/generate` with SSE streaming), prompt builder with session context, and multi-agent CLI dispatch - **Date range analysis**: insights can cover a single day or a date range (e.g., a week). DB schema uses `date_from`/`date_to` columns. The generate endpoint validates both fields and supports `date_to >= date_from` - **Mode-driven UI**: type selector with three modes -- Daily Activity (single date), Date Range Activity (from/to inputs with 7-day and 30-day presets), and Agent Analysis (single date). Mode is derived reactively from store state - Svelte 5 Insights page with sidebar controls (type, date, project, agent), concurrent generation tasks with live status spinners, date display on in-progress tasks, and markdown content viewer with delete support - **Structured API errors**: `ApiError` class with `status` field for programmatic error handling; empty response bodies fall back to `"API <status>"` message - **ListInsights capped at 500 rows** with `created_at DESC` index to prevent unbounded queries - `internal/insight` package: prompt construction from session data with date-aware text ("Date" vs "Date Range"), streaming response parsing for Claude/Codex/Gemini CLI output - **Session breadcrumb bar** showing project name, agent badge (color-coded), and session start time - **Header navigation** with always-visible Sessions/Insights buttons and active state highlighting - Design polish: Inter font, refined color tokens for light/dark themes, tighter typography and spacing ## Test plan - [x] `go test -tags fts5 ./...` -- all Go tests pass (insights CRUD, filters, 500-row cap, date range round-trip, prompt builder with single/range dates, server handler validation) - [x] `npx vitest run` -- all 360 frontend tests pass (insights store, date sync, mode switching, generate payloads, ApiError handling with empty-body fallback) - [x] `npx tsc --noEmit` -- no type errors - [x] `go vet ./...` -- clean - [ ] Manual: generate a Daily Activity insight, verify single date in list and content header - [ ] Manual: switch to Date Range Activity, verify from/to inputs and presets appear, generate and verify range displays - [ ] Manual: verify all insights appear in the list regardless of selected generation dates - [ ] Manual: verify in-progress task items show the date being analyzed - [ ] Manual: delete an insight, verify removal from list and content area Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 07f572a commit ab63cbd

30 files changed

+4926
-95
lines changed

.roborev.toml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[review_guidelines]
2+
3+
threat_model = """
4+
agentsview is a LOCAL-ONLY developer tool. It binds to 127.0.0.1
5+
by default and is not designed for multi-user or public deployment.
6+
7+
Key assumptions reviewers MUST account for:
8+
9+
1. NO AUTHENTICATION NEEDED: All API endpoints are unauthenticated
10+
by design. The server listens on localhost only. Flagging missing
11+
auth on any endpoint is a false positive.
12+
13+
2. XSS / SANITIZATION: Markdown rendering uses DOMPurify. The
14+
{@html renderMarkdown(...)} pattern in Svelte is intentional and
15+
safe because renderMarkdown() sanitizes via DOMPurify before
16+
returning HTML. Do not flag this as XSS.
17+
18+
3. RATE LIMITING / DoS: As a local single-user tool, rate limiting
19+
and concurrency caps are unnecessary. The 10-minute timeout on
20+
AI CLI generation is intentional. Do not flag missing rate limits.
21+
22+
4. CORS: The CORS policy uses Allow-Origin: * because the embedded
23+
SPA is served from the same origin. Cross-origin access from
24+
other local tools is acceptable for a localhost-only service.
25+
26+
5. INPUT VALIDATION: Request body size limits are not required for
27+
a localhost-only service. Do not flag missing MaxBytesReader or
28+
similar unless the endpoint is exposed to untrusted networks.
29+
30+
Do NOT flag issues that only apply to public-facing, multi-tenant,
31+
or network-exposed services. Focus on bugs, logic errors, data
32+
corruption risks, and code quality issues.
33+
"""

frontend/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
77
<link rel="preconnect" href="https://fonts.googleapis.com">
88
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9-
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
9+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400..700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
1010
<title>AgentsView</title>
1111
</head>
1212
<body>

frontend/src/App.svelte

Lines changed: 126 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import ShortcutsModal from "./lib/components/modals/ShortcutsModal.svelte";
1010
import PublishModal from "./lib/components/modals/PublishModal.svelte";
1111
import AnalyticsPage from "./lib/components/analytics/AnalyticsPage.svelte";
12+
import InsightsPage from "./lib/components/insights/InsightsPage.svelte";
1213
import { sessions } from "./lib/stores/sessions.svelte.js";
1314
import { messages } from "./lib/stores/messages.svelte.js";
1415
import { sync } from "./lib/stores/sync.svelte.js";
@@ -150,19 +151,55 @@
150151

151152
<AppHeader />
152153

153-
<ThreeColumnLayout>
154-
{#snippet sidebar()}
155-
<SessionList />
156-
{/snippet}
154+
{#if router.route === "insights"}
155+
<InsightsPage />
156+
{:else}
157+
<ThreeColumnLayout>
158+
{#snippet sidebar()}
159+
<SessionList />
160+
{/snippet}
157161

158-
{#snippet content()}
159-
{#if sessions.activeSessionId}
160-
<MessageList bind:this={messageListRef} />
161-
{:else}
162-
<AnalyticsPage />
163-
{/if}
164-
{/snippet}
165-
</ThreeColumnLayout>
162+
{#snippet content()}
163+
{#if sessions.activeSessionId}
164+
{@const session = sessions.activeSession}
165+
<div class="session-breadcrumb">
166+
<button
167+
class="breadcrumb-link"
168+
onclick={() => sessions.deselectSession()}
169+
>Sessions</button>
170+
<span class="breadcrumb-sep">/</span>
171+
<span class="breadcrumb-current">
172+
{session?.project ?? ""}
173+
</span>
174+
{#if session}
175+
<span class="breadcrumb-meta">
176+
<span
177+
class="agent-badge"
178+
class:agent-claude={session.agent === "claude"}
179+
class:agent-codex={session.agent === "codex"}
180+
>{session.agent}</span>
181+
{#if session.started_at}
182+
<span class="session-time">
183+
{new Date(session.started_at).toLocaleDateString(undefined, {
184+
month: "short",
185+
day: "numeric",
186+
})}
187+
{new Date(session.started_at).toLocaleTimeString(undefined, {
188+
hour: "2-digit",
189+
minute: "2-digit",
190+
})}
191+
</span>
192+
{/if}
193+
</span>
194+
{/if}
195+
</div>
196+
<MessageList bind:this={messageListRef} />
197+
{:else}
198+
<AnalyticsPage />
199+
{/if}
200+
{/snippet}
201+
</ThreeColumnLayout>
202+
{/if}
166203

167204
<StatusBar />
168205

@@ -177,3 +214,80 @@
177214
{#if ui.activeModal === "publish"}
178215
<PublishModal />
179216
{/if}
217+
218+
<style>
219+
.session-breadcrumb {
220+
display: flex;
221+
align-items: center;
222+
gap: 6px;
223+
height: 32px;
224+
padding: 0 14px;
225+
border-bottom: 1px solid var(--border-muted);
226+
flex-shrink: 0;
227+
font-size: 11px;
228+
color: var(--text-muted);
229+
}
230+
231+
.breadcrumb-link {
232+
color: var(--text-muted);
233+
font-size: 11px;
234+
font-weight: 500;
235+
cursor: pointer;
236+
transition: color 0.12s;
237+
}
238+
239+
.breadcrumb-link:hover {
240+
color: var(--accent-blue);
241+
}
242+
243+
.breadcrumb-sep {
244+
opacity: 0.3;
245+
font-size: 10px;
246+
}
247+
248+
.breadcrumb-current {
249+
color: var(--text-primary);
250+
font-weight: 500;
251+
white-space: nowrap;
252+
overflow: hidden;
253+
text-overflow: ellipsis;
254+
flex: 1;
255+
min-width: 0;
256+
}
257+
258+
.breadcrumb-meta {
259+
display: flex;
260+
align-items: center;
261+
gap: 6px;
262+
margin-left: auto;
263+
flex-shrink: 0;
264+
}
265+
266+
.agent-badge {
267+
font-size: 9px;
268+
font-weight: 600;
269+
padding: 1px 6px;
270+
border-radius: 8px;
271+
text-transform: uppercase;
272+
letter-spacing: 0.03em;
273+
color: white;
274+
flex-shrink: 0;
275+
background: var(--text-muted);
276+
}
277+
278+
.agent-claude {
279+
background: var(--accent-blue);
280+
}
281+
282+
.agent-codex {
283+
background: var(--accent-green);
284+
}
285+
286+
.session-time {
287+
font-size: 10px;
288+
color: var(--text-muted);
289+
font-variant-numeric: tabular-nums;
290+
white-space: nowrap;
291+
flex-shrink: 0;
292+
}
293+
</style>

frontend/src/app.css

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@
99

1010
/* Light theme (default) — cool, high-contrast, readable */
1111
:root {
12-
--bg-primary: #f7f7fa;
12+
--bg-primary: #f5f6f8;
1313
--bg-surface: #ffffff;
14-
--bg-surface-hover: #f0f1f5;
15-
--bg-inset: #edeef3;
16-
--border-default: #dfe1e8;
17-
--border-muted: #e8eaf0;
18-
--text-primary: #1a1d26;
19-
--text-secondary: #5a6070;
20-
--text-muted: #8b92a0;
14+
--bg-surface-hover: #f0f1f4;
15+
--bg-inset: #ecedf2;
16+
--border-default: #d8dae2;
17+
--border-muted: #e4e6ec;
18+
--text-primary: #181b24;
19+
--text-secondary: #555b6e;
20+
--text-muted: #878ea0;
2121
--accent-blue: #2563eb;
2222
--accent-purple: #7c3aed;
2323
--accent-amber: #d97706;
@@ -29,13 +29,14 @@
2929
--tool-bg: #fffbf0;
3030
--code-bg: #1e1e2e;
3131
--code-text: #cdd6f4;
32-
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
33-
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.06);
32+
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
33+
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.08);
34+
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.1);
3435
--radius-sm: 4px;
3536
--radius-md: 6px;
3637
--radius-lg: 8px;
37-
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI",
38-
"Noto Sans", Helvetica, Arial, sans-serif;
38+
--font-sans: "Inter", -apple-system, BlinkMacSystemFont,
39+
"Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
3940
--font-mono: "JetBrains Mono", "SF Mono", "Fira Code",
4041
"Fira Mono", Menlo, Consolas, monospace;
4142
--viewport-indicator: rgba(0, 0, 0, 0.08);
@@ -45,15 +46,15 @@
4546

4647
/* Dark theme */
4748
:root.dark {
48-
--bg-primary: #0c0c10;
49-
--bg-surface: #15151b;
50-
--bg-surface-hover: #1e1e28;
51-
--bg-inset: #101015;
52-
--border-default: #2a2a35;
53-
--border-muted: #222230;
54-
--text-primary: #e2e4e9;
55-
--text-secondary: #9ca3af;
56-
--text-muted: #6b7280;
49+
--bg-primary: #0d0d12;
50+
--bg-surface: #16161e;
51+
--bg-surface-hover: #1f1f2a;
52+
--bg-inset: #111116;
53+
--border-default: #2b2b38;
54+
--border-muted: #232330;
55+
--text-primary: #e4e6eb;
56+
--text-secondary: #9ea5b4;
57+
--text-muted: #6c7385;
5758
--accent-blue: #60a5fa;
5859
--accent-purple: #a78bfa;
5960
--accent-amber: #fbbf24;
@@ -65,8 +66,9 @@
6566
--tool-bg: #1a1508;
6667
--code-bg: #0d0d14;
6768
--code-text: #cdd6f4;
68-
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2);
69-
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.3);
69+
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.25);
70+
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.35);
71+
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.4);
7072
--viewport-indicator: rgba(255, 255, 255, 0.08);
7173
--overlay-bg: rgba(0, 0, 0, 0.6);
7274
color-scheme: dark;
@@ -77,12 +79,14 @@ body {
7779
height: 100%;
7880
overflow: hidden;
7981
font-family: var(--font-sans);
80-
font-size: 14px;
82+
font-size: 13px;
8183
line-height: 1.5;
8284
color: var(--text-primary);
8385
background: var(--bg-primary);
8486
-webkit-font-smoothing: antialiased;
8587
-moz-osx-font-smoothing: grayscale;
88+
font-feature-settings: "cv11", "ss01";
89+
text-rendering: optimizeLegibility;
8690
}
8791

8892
#app {

0 commit comments

Comments
 (0)