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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.5] - 2026-04-25

### Fixed
- **Full-text search returned 0 results** for any project ingested after the `pipeline → stats` rename in 0.3.0. `SearchService.reindex_all`, `TagService.reindex_all`, and `QaService.reindex_all` all imported `from ..pipeline import process` — module no longer exists. Hitting the Reindex button silently failed for the entire run. Replaced with `queries.get_project_stats(conn, project_id=...)`.
- **Reindex was wiping its own work for duplicate slugs** — schema has `UNIQUE(provider, slug)` so a project used through both Claude and Codex has two rows with the same slug. `index_project` does `DELETE WHERE project = ?` before inserting, so iterating rows naively had iteration 2 wipe iteration 1. Now grouped by slug and concatenated before indexing. Verified live: chimera-scoped search for "refactor" returns 74 hits (was 0).

### Changed
- **UX pass on the dashboard:**
- Cost-tab table page-size default 25 → 10.
- `TokenCompositionDonut` height 260 → 360, radii 60/95 → 85/135 — reads as a hero card instead of a small chart in a big box.
- `Top Sessions by Cost` y-axis labels: `<short_id> · <first prompt preview>` instead of bare hash.
- Overview "Date Range" mini-card: `Jan 30, 2026 → to Apr 25, 2026` instead of raw ISO slice (`01-30T20:58:11.193Z`).
- Overview layout: `CacheRoiCard` and `TokenCompositionDonut` share a 2-col grid on lg+ instead of stacking full-width.
- Per-message JSON toggle in the session viewer — independent of the global Raw JSON / Formatted switch so users can drill into one message without flipping the whole view.
- **Commands and Messages tab content cells wrap** instead of truncating to a single line. Slice limits 200 → 400 (Commands) and 150 → 300 (Messages).
- **All Overview chart heights standardised at 280** (some were 250, mixing). `ToolUsageBarChart` was unbounded (`Math.max(250, n*32)` → 700+ on busy projects); now `min(420, max(280, n*28))`.

## [0.3.4] - 2026-04-25

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
# with the real hash that nix reports on first build.
frontend = pkgs.buildNpmPackage {
pname = "stackunderflow-ui";
version = "0.3.4";
version = "0.3.5";
src = ./stackunderflow-ui;
npmDepsHash = "sha256-QCAlYLx7LP2702pTBwi3N8Ft4hFqP88xMCRoV+h1jls=";

Expand Down Expand Up @@ -48,7 +48,7 @@
# ── Python package ──────────────────────────────────────────
stackunderflow-pkg = pp.buildPythonPackage {
pname = "stackunderflow";
version = "0.3.4";
version = "0.3.5";
pyproject = true;
src = srcWithFrontend;

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "stackunderflow"
version = "0.3.4"
version = "0.3.5"
description = "A local-first knowledge base for your AI coding sessions"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
2 changes: 1 addition & 1 deletion stackunderflow-ui/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "stackunderflow-ui",
"private": true,
"version": "0.3.4",
"version": "0.3.5",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default function CommandToolDistChart({ toolCountDist }: CommandToolDistC
return (
<div className="bg-gray-100/70 dark:bg-gray-800/50 rounded-lg p-4 border border-gray-200 dark:border-gray-800">
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Commands by Tool Count</h3>
<ResponsiveContainer width="100%" height={250}>
<ResponsiveContainer width="100%" height={280}>
<BarChart data={data}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis
Expand Down
2 changes: 1 addition & 1 deletion stackunderflow-ui/src/components/charts/DailyCostChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default function DailyCostChart({ dailyStats }: DailyCostChartProps) {
return (
<div className="bg-gray-100/70 dark:bg-gray-800/50 rounded-lg p-4 border border-gray-200 dark:border-gray-800">
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Daily Cost</h3>
<ResponsiveContainer width="100%" height={250}>
<ResponsiveContainer width="100%" height={280}>
<BarChart data={data}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default function ErrorDistributionChart({ errorCategories }: ErrorDistrib
Error Categories
<span className="ml-2 text-xs text-gray-500 font-normal">{total} total</span>
</h3>
<ResponsiveContainer width="100%" height={250}>
<ResponsiveContainer width="100%" height={280}>
<BarChart data={data} layout="vertical" margin={{ left: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" horizontal={false} />
<XAxis
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default function HourlyPatternChart({ hourlyPattern }: HourlyPatternChart
return (
<div className="bg-gray-100/70 dark:bg-gray-800/50 rounded-lg p-4 border border-gray-200 dark:border-gray-800">
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Hourly Token Pattern</h3>
<ResponsiveContainer width="100%" height={250}>
<ResponsiveContainer width="100%" height={280}>
<ComposedChart data={data}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default function ModelDistributionChart({ modelStats }: ModelDistribution
return (
<div className="bg-gray-100/70 dark:bg-gray-800/50 rounded-lg p-4 border border-gray-200 dark:border-gray-800">
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Token Distribution by Model</h3>
<ResponsiveContainer width="100%" height={250}>
<ResponsiveContainer width="100%" height={280}>
<PieChart>
<Pie
data={data}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default function TokenUsageChart({ dailyStats }: TokenUsageChartProps) {
return (
<div className="bg-gray-100/70 dark:bg-gray-800/50 rounded-lg p-4 border border-gray-200 dark:border-gray-800">
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Daily Token Usage</h3>
<ResponsiveContainer width="100%" height={250}>
<ResponsiveContainer width="100%" height={280}>
<AreaChart data={data}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default function ToolUsageBarChart({ toolStats }: ToolUsageBarChartProps)
return (
<div className="bg-gray-100/70 dark:bg-gray-800/50 rounded-lg p-4 border border-gray-200 dark:border-gray-800">
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Top Tools by Usage</h3>
<ResponsiveContainer width="100%" height={Math.max(250, data.length * 32)}>
<ResponsiveContainer width="100%" height={Math.min(420, Math.max(280, data.length * 28))}>
<BarChart data={data} layout="vertical" margin={{ left: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" horizontal={false} />
<XAxis
Expand Down
2 changes: 1 addition & 1 deletion stackunderflow-ui/src/components/charts/ToolUsageChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default function ToolUsageChart({ toolStats }: ToolUsageChartProps) {
return (
<div className="bg-gray-100/70 dark:bg-gray-800/50 rounded-lg p-4 border border-gray-200 dark:border-gray-800">
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Top Tool Usage</h3>
<ResponsiveContainer width="100%" height={250}>
<ResponsiveContainer width="100%" height={280}>
<BarChart data={data} layout="vertical" margin={{ left: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" horizontal={false} />
<XAxis
Expand Down
2 changes: 1 addition & 1 deletion stackunderflow-ui/src/components/cost/CommandCostList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ export default function CommandCostList({ data, onOpen, initialSort }: CommandCo

const [expanded, setExpanded] = useState<Set<string>>(() => new Set())
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(25)
const [pageSize, setPageSize] = useState(10)
const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize))
const safePage = Math.min(page, totalPages)
const paged = useMemo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ function formatTime(iso: string): string {
})
}

const DEFAULT_PAGE_SIZE = 50
const DEFAULT_PAGE_SIZE = 10

type SortableKey = keyof Pick<
OutlierCommand,
Expand Down
2 changes: 1 addition & 1 deletion stackunderflow-ui/src/components/cost/RetryAlertsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const FILTERS: Array<{ id: SeverityFilter; label: string; predicate: (s: RetrySi
export default function RetryAlertsPanel({ signals }: RetryAlertsPanelProps) {
const [filter, setFilter] = useState<SeverityFilter>('all')
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(25)
const [pageSize, setPageSize] = useState(10)

const sortedAll = useMemo(() => {
if (!signals || signals.length === 0) return []
Expand Down
39 changes: 24 additions & 15 deletions stackunderflow-ui/src/components/cost/SessionCostBarChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ function formatTokens(n: number): string {
interface ChartDatum {
session_id: string
short_id: string
/** Pre-formatted Y-axis label: "534bba1f · refactor the auth …" */
label: string
cost: number
commands: number
errors: number
Expand Down Expand Up @@ -186,18 +188,24 @@ export default function SessionCostBarChart({ data, onSelect }: SessionCostBarCh
const chartData: ChartDatum[] = [...data]
.sort((a, b) => b.cost - a.cost)
.slice(0, 10)
.map((s) => ({
session_id: s.session_id,
short_id: shortSession(s.session_id),
cost: s.cost,
commands: s.commands,
errors: s.errors,
messages: s.messages,
duration_s: s.duration_s,
models_used: s.models_used ?? [],
tokens: s.tokens ?? {},
preview: s.first_prompt_preview,
}))
.map((s) => {
const sid = shortSession(s.session_id)
const preview = (s.first_prompt_preview ?? '').replace(/\s+/g, ' ').trim()
const truncated = preview.length > 36 ? preview.slice(0, 36) + '…' : preview
return {
session_id: s.session_id,
short_id: sid,
label: truncated ? `${sid} · ${truncated}` : sid,
cost: s.cost,
commands: s.commands,
errors: s.errors,
messages: s.messages,
duration_s: s.duration_s,
models_used: s.models_used ?? [],
tokens: s.tokens ?? {},
preview: s.first_prompt_preview,
}
})

const maxCost = chartData.reduce((m, d) => (d.cost > m ? d.cost : m), 0)
// Only label bars that are > 10% of the chart max.
Expand Down Expand Up @@ -231,11 +239,12 @@ export default function SessionCostBarChart({ data, onSelect }: SessionCostBarCh
/>
<YAxis
type="category"
dataKey="short_id"
tick={{ fontSize: 10, fill: '#9CA3AF', fontFamily: 'monospace' }}
dataKey="label"
tick={{ fontSize: 11, fill: '#9CA3AF' }}
tickLine={{ stroke: '#4B5563' }}
axisLine={{ stroke: '#4B5563' }}
width={80}
width={320}
interval={0}
/>
<Tooltip
content={<SessionTooltip />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,14 +147,14 @@ export default function TokenCompositionDonut({ totals }: TokenCompositionDonutP
{formatTokens(grandTotal)} total
</span>
</h3>
<ResponsiveContainer width="100%" height={260}>
<ResponsiveContainer width="100%" height={360}>
<PieChart>
<Pie
data={data}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={95}
innerRadius={85}
outerRadius={135}
paddingAngle={2}
dataKey="value"
nameKey="label"
Expand Down
6 changes: 3 additions & 3 deletions stackunderflow-ui/src/components/dashboard/CommandsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,9 @@ export default function CommandsTab({ data }: CommandsTabProps) {
) : (
<IconChevronRight size={14} className="text-gray-500 mt-0.5 shrink-0" />
)}
<span className="text-gray-700 dark:text-gray-300 truncate">
{row.command.content.length > 200
? row.command.content.slice(0, 200) + '...'
<span className="text-gray-700 dark:text-gray-300 whitespace-pre-wrap break-words min-w-0">
{row.command.content.length > 400
? row.command.content.slice(0, 400) + ''
: row.command.content}
</span>
</div>
Expand Down
6 changes: 3 additions & 3 deletions stackunderflow-ui/src/components/dashboard/MessagesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,9 +258,9 @@ export default function MessagesTab({ data, projectName }: MessagesTabProps) {
key: 'content',
label: 'Content',
render: (row) => (
<span className="text-gray-700 dark:text-gray-300 text-xs truncate block max-w-lg">
{row.content.length > 150
? row.content.slice(0, 150) + '...'
<span className="text-gray-700 dark:text-gray-300 text-xs whitespace-pre-wrap break-words block">
{row.content.length > 300
? row.content.slice(0, 300) + ''
: row.content}
</span>
),
Expand Down
30 changes: 22 additions & 8 deletions stackunderflow-ui/src/components/dashboard/OverviewTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,20 @@ function formatNumber(n: number): string {
return n.toLocaleString()
}

/**
* "2026-01-30T20:58:11.193Z" → "Jan 30, 2026". Falls through to the original
* string if it's not a parseable ISO timestamp so we never blank out a label.
*/
function formatDateRange(iso: string): string {
const d = new Date(iso)
if (Number.isNaN(d.getTime())) return iso
return d.toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
})
}

import { formatCost } from '../../services/format'

interface MiniStatCardProps {
Expand Down Expand Up @@ -138,12 +152,12 @@ export default function OverviewTab({ stats }: OverviewTabProps) {
{/* Primary stats from existing StatsCards component */}
<StatsCards stats={stats} />

{/* Cache ROI hero card — uses the still-present `cache` field on
/api/dashboard-data; daily_stats supplies the ROI sparkline. */}
<CacheRoiCard cache={stats.cache} dailyStats={stats.daily_stats} />

{/* Token composition donut replaces the four mini token cards (spec §2.4) */}
<TokenCompositionDonut totals={tokenTotals} />
{/* Cache ROI + Token composition share a row on wide screens so the
donut doesn't stretch to a full-width band on its own. */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<CacheRoiCard cache={stats.cache} dailyStats={stats.daily_stats} />
<TokenCompositionDonut totals={tokenTotals} />
</div>

{/* Extended stat cards grid */}
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
Expand Down Expand Up @@ -211,8 +225,8 @@ export default function OverviewTab({ stats }: OverviewTabProps) {
<MiniStatCard
icon={<IconCalendar size={14} />}
label="Date Range"
value={dateRange.start ? `${dateRange.start.slice(5)}` : 'N/A'}
sublabel={dateRange.end ? `to ${dateRange.end.slice(5)}` : ''}
value={dateRange.start ? formatDateRange(dateRange.start) : 'N/A'}
sublabel={dateRange.end ? `to ${formatDateRange(dateRange.end)}` : ''}
color="text-gray-600 dark:text-gray-400"
/>
</div>
Expand Down
22 changes: 22 additions & 0 deletions stackunderflow-ui/src/components/dashboard/SessionsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,9 @@ function ConversationMessage({
isFirstInSidechainGroup?: boolean
}) {
const [expanded, setExpanded] = useState(false)
// Per-message JSON disclosure. Independent from the global `showRaw` toggle
// so users can drill into one message without flipping the whole session.
const [showJson, setShowJson] = useState(false)
const role = getRole(line)
const content = getContent(line)
const ts = getTimestamp(line)
Expand Down Expand Up @@ -425,6 +428,20 @@ function ConversationMessage({
)}
<span className="flex-1" />
{ts && <span className="text-[10px] text-gray-600 dark:text-gray-400">{fmtTs(ts)}</span>}
<button
type="button"
onClick={() => setShowJson((v) => !v)}
aria-pressed={showJson}
aria-label={showJson ? 'Hide raw JSON' : 'View raw JSON'}
title={showJson ? 'Hide raw JSON' : 'View raw JSON'}
className={`text-[10px] px-1.5 py-0.5 rounded border transition-colors ${
showJson
? 'bg-amber-100 text-amber-800 border-amber-300 dark:bg-amber-900/40 dark:text-amber-300 dark:border-amber-800'
: 'bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700 hover:text-gray-800 dark:hover:text-gray-200'
}`}
>
{showJson ? 'Hide JSON' : 'JSON'}
</button>
</div>
{/* Body */}
<div className="px-4 py-3">
Expand All @@ -447,6 +464,11 @@ function ConversationMessage({
Show less
</button>
)}
{showJson && (
<pre className="mt-3 text-[11px] text-gray-600 dark:text-gray-400 overflow-x-auto whitespace-pre-wrap font-mono bg-gray-50/70 dark:bg-gray-950/60 rounded p-2 max-h-96 overflow-y-auto border border-gray-200 dark:border-gray-800">
{JSON.stringify(line, null, 2)}
</pre>
)}
</div>
</div>
)
Expand Down
2 changes: 1 addition & 1 deletion stackunderflow/__version__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Version information for stackunderflow"""

__version__ = "0.3.4"
__version__ = "0.3.5"
Loading
Loading