diff --git a/package-lock.json b/package-lock.json index eefe6eb0..5ec2ef4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -96,9 +96,9 @@ } }, "node_modules/@anthropic-ai/claude-agent-sdk": { - "version": "0.2.91", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.91.tgz", - "integrity": "sha512-DCd5Ad5XKBbIIOMZ73L+c+e9azM6NtZzOtdKQAzykzRG/KxSCMraMAsMMQrJrIUMH3oTtHY7QuQimAiElVVVpA==", + "version": "0.2.98", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.98.tgz", + "integrity": "sha512-pWUx+xY21rKy5wvX0eBZja7p8J5ykOYaHsykvdj9nkTbAVXmP1WusI1mP6jbBByJ8uBJeBc4beAPSZIFcdIpTA==", "license": "SEE LICENSE IN README.md", "dependencies": { "@anthropic-ai/sdk": "^0.80.0", @@ -2137,9 +2137,9 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.12", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.12.tgz", - "integrity": "sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw==", + "version": "1.19.13", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz", + "integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==", "license": "MIT", "engines": { "node": ">=18.14.1" @@ -4915,14 +4915,14 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", - "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "proxy-from-env": "^2.1.0" } }, "node_modules/b4a": { @@ -7558,9 +7558,9 @@ } }, "node_modules/hono": { - "version": "4.12.9", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz", - "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==", + "version": "4.12.12", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", + "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -9513,8 +9513,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/pump": { "version": "3.0.3", @@ -10787,9 +10792,9 @@ } }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/tests/unit/cli/credential-scoping.test.ts b/tests/unit/cli/credential-scoping.test.ts index f1626e6f..52f53251 100644 --- a/tests/unit/cli/credential-scoping.test.ts +++ b/tests/unit/cli/credential-scoping.test.ts @@ -68,9 +68,10 @@ describe('CredentialScopedCommand', () => { beforeEach(() => { process.env = { ...originalEnv }; - process.env.GITHUB_TOKEN = undefined; - process.env.TRELLO_API_KEY = undefined; - process.env.TRELLO_TOKEN = undefined; + delete process.env.GITHUB_TOKEN; + delete process.env.GITHUB_TOKEN_IMPLEMENTER; + delete process.env.TRELLO_API_KEY; + delete process.env.TRELLO_TOKEN; }); afterEach(() => { diff --git a/tests/unit/cli/dashboard/webhooks/webhooks.test.ts b/tests/unit/cli/dashboard/webhooks/webhooks.test.ts index 4879cf7f..5e12f391 100644 --- a/tests/unit/cli/dashboard/webhooks/webhooks.test.ts +++ b/tests/unit/cli/dashboard/webhooks/webhooks.test.ts @@ -163,6 +163,7 @@ describe('WebhooksCreate (webhooks create)', () => { callbackBaseUrl: baseConfig.serverUrl, trelloOnly: false, githubOnly: false, + gitlabOnly: false, oneTimeTokens: undefined, }); }); @@ -182,6 +183,7 @@ describe('WebhooksCreate (webhooks create)', () => { callbackBaseUrl: 'https://cascade.example.com', trelloOnly: false, githubOnly: false, + gitlabOnly: false, oneTimeTokens: undefined, }); }); @@ -201,6 +203,7 @@ describe('WebhooksCreate (webhooks create)', () => { callbackBaseUrl: baseConfig.serverUrl, trelloOnly: false, githubOnly: false, + gitlabOnly: false, oneTimeTokens: { github: 'ghp_testtoken123' }, }); }); @@ -242,6 +245,7 @@ describe('WebhooksDelete (webhooks delete)', () => { callbackBaseUrl: baseConfig.serverUrl, trelloOnly: false, githubOnly: false, + gitlabOnly: false, oneTimeTokens: undefined, }); }); @@ -261,6 +265,7 @@ describe('WebhooksDelete (webhooks delete)', () => { callbackBaseUrl: 'https://cascade.example.com', trelloOnly: false, githubOnly: false, + gitlabOnly: false, oneTimeTokens: undefined, }); }); @@ -280,6 +285,7 @@ describe('WebhooksDelete (webhooks delete)', () => { callbackBaseUrl: baseConfig.serverUrl, trelloOnly: false, githubOnly: false, + gitlabOnly: false, oneTimeTokens: { github: 'ghp_testtoken123' }, }); }); diff --git a/web/src/components/projects/project-work-table.tsx b/web/src/components/projects/project-work-table.tsx index 8b224367..08a250ab 100644 --- a/web/src/components/projects/project-work-table.tsx +++ b/web/src/components/projects/project-work-table.tsx @@ -1,5 +1,6 @@ import { useNavigate } from '@tanstack/react-router'; import { ClipboardList, ExternalLink, GitPullRequest } from 'lucide-react'; +import { useTheme } from 'next-themes'; import { agentTypeLabel, getAgentColor } from '@/lib/chart-colors.js'; import { formatCostSummary } from '@/lib/utils.js'; import { WorkItemDurationBar } from './work-item-duration-bar.js'; @@ -234,6 +235,8 @@ export function ProjectWorkTable({ onPageChange, projectAvgDurationMs, }: ProjectWorkTableProps) { + const { resolvedTheme } = useTheme(); + const isDark = resolvedTheme === 'dark'; const total = items.length; const totalPages = Math.ceil(total / limit); const currentPage = Math.floor(offset / limit) + 1; @@ -257,7 +260,7 @@ export function ProjectWorkTable({ width: 10, height: 10, borderRadius: 2, - background: getAgentColor(at), + background: getAgentColor(at, isDark), flexShrink: 0, }} /> diff --git a/web/src/components/projects/work-item-duration-bar.tsx b/web/src/components/projects/work-item-duration-bar.tsx index a26f4159..de5431fb 100644 --- a/web/src/components/projects/work-item-duration-bar.tsx +++ b/web/src/components/projects/work-item-duration-bar.tsx @@ -1,3 +1,4 @@ +import { useTheme } from 'next-themes'; import { agentTypeLabel, getAgentColor } from '@/lib/chart-colors.js'; import { formatDuration } from '@/lib/utils.js'; @@ -22,7 +23,7 @@ export interface DurationSegment { * * Exported for testability. */ -export function buildDurationSegments(runs: RunSegmentInput[]): DurationSegment[] { +export function buildDurationSegments(runs: RunSegmentInput[], dark = false): DurationSegment[] { const runsWithDuration = runs.filter((r) => r.durationMs > 0); if (runsWithDuration.length === 0) return []; @@ -36,7 +37,7 @@ export function buildDurationSegments(runs: RunSegmentInput[]): DurationSegment[ agentType: run.agentType, durationMs: run.durationMs, status: run.status, - color: getAgentColor(run.agentType), + color: getAgentColor(run.agentType, dark), pct: totalMs > 0 ? (run.durationMs / totalMs) * 100 : 0, label: `${agentTypeLabel(run.agentType)} #${count}`, }; @@ -53,7 +54,9 @@ interface WorkItemDurationBarProps { * Highlights in red when total > 2× project average (outlier). */ export function WorkItemDurationBar({ runs, projectAvgDurationMs }: WorkItemDurationBarProps) { - const segments = buildDurationSegments(runs); + const { resolvedTheme } = useTheme(); + const isDark = resolvedTheme === 'dark'; + const segments = buildDurationSegments(runs, isDark); if (segments.length === 0) { return ; diff --git a/web/src/components/runs/project-work-duration-chart.tsx b/web/src/components/runs/project-work-duration-chart.tsx index ee2577ca..6441bdd0 100644 --- a/web/src/components/runs/project-work-duration-chart.tsx +++ b/web/src/components/runs/project-work-duration-chart.tsx @@ -1,3 +1,4 @@ +import { useTheme } from 'next-themes'; import { Bar, BarChart, @@ -34,7 +35,10 @@ interface ChartEntry { color: string; } -export function buildDurationChartData(byAgentType: AgentTypeBreakdown[]): ChartEntry[] { +export function buildDurationChartData( + byAgentType: AgentTypeBreakdown[], + dark = false, +): ChartEntry[] { return byAgentType .filter((breakdown) => breakdown.totalDurationMs > 0) .map((breakdown) => ({ @@ -43,13 +47,15 @@ export function buildDurationChartData(byAgentType: AgentTypeBreakdown[]): Chart totalDurationMs: breakdown.totalDurationMs, runCount: breakdown.runCount, avgDurationMs: breakdown.avgDurationMs ?? 0, - color: getAgentColor(breakdown.agentType), + color: getAgentColor(breakdown.agentType, dark), })) .sort((a, b) => b.totalDurationMs - a.totalDurationMs); } export function ProjectWorkDurationChart({ byAgentType }: ProjectWorkDurationChartProps) { - const data: ChartEntry[] = buildDurationChartData(byAgentType); + const { resolvedTheme } = useTheme(); + const isDark = resolvedTheme === 'dark'; + const data: ChartEntry[] = buildDurationChartData(byAgentType, isDark); if (data.length === 0) { return ( diff --git a/web/src/components/runs/work-item-cost-chart.tsx b/web/src/components/runs/work-item-cost-chart.tsx index 630efe36..7a431025 100644 --- a/web/src/components/runs/work-item-cost-chart.tsx +++ b/web/src/components/runs/work-item-cost-chart.tsx @@ -1,3 +1,4 @@ +import { useTheme } from 'next-themes'; import { Cell, Label, Legend, Pie, PieChart, ResponsiveContainer, Tooltip } from 'recharts'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card.js'; import { agentTypeLabel, getAgentColor } from '@/lib/chart-colors.js'; @@ -28,7 +29,7 @@ interface CostEntry { color: string; } -function buildDataFromRuns(runs: WorkItemRun[]): CostEntry[] { +function buildDataFromRuns(runs: WorkItemRun[], dark: boolean): CostEntry[] { const costByAgent: Record = {}; for (const run of runs) { if (run.costUsd != null) { @@ -42,11 +43,11 @@ function buildDataFromRuns(runs: WorkItemRun[]): CostEntry[] { name: agentTypeLabel(agentType), agentType, value, - color: getAgentColor(agentType), + color: getAgentColor(agentType, dark), })); } -function buildDataFromBreakdown(byAgentType: AgentTypeBreakdown[]): CostEntry[] { +function buildDataFromBreakdown(byAgentType: AgentTypeBreakdown[], dark: boolean): CostEntry[] { return byAgentType .map((breakdown) => { const cost = Number.parseFloat(breakdown.totalCostUsd); @@ -54,16 +55,18 @@ function buildDataFromBreakdown(byAgentType: AgentTypeBreakdown[]): CostEntry[] name: agentTypeLabel(breakdown.agentType), agentType: breakdown.agentType, value: Number.isNaN(cost) ? 0 : cost, - color: getAgentColor(breakdown.agentType), + color: getAgentColor(breakdown.agentType, dark), }; }) .filter((entry) => entry.value > 0); } export function WorkItemCostChart({ runs, byAgentType }: WorkItemCostChartProps) { + const { resolvedTheme } = useTheme(); + const isDark = resolvedTheme === 'dark'; const data: CostEntry[] = byAgentType - ? buildDataFromBreakdown(byAgentType) - : buildDataFromRuns(runs ?? []); + ? buildDataFromBreakdown(byAgentType, isDark) + : buildDataFromRuns(runs ?? [], isDark); const totalCost = data.reduce((sum, d) => sum + d.value, 0); diff --git a/web/src/components/runs/work-item-duration-chart.tsx b/web/src/components/runs/work-item-duration-chart.tsx index 302bc33a..9339730c 100644 --- a/web/src/components/runs/work-item-duration-chart.tsx +++ b/web/src/components/runs/work-item-duration-chart.tsx @@ -1,3 +1,4 @@ +import { useTheme } from 'next-themes'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card.js'; import { agentTypeLabel, getAgentColor } from '@/lib/chart-colors.js'; import { formatDuration } from '@/lib/utils.js'; @@ -29,6 +30,8 @@ interface SegmentEntry { } export function WorkItemDurationChart({ runs }: WorkItemDurationChartProps) { + const { resolvedTheme } = useTheme(); + const isDark = resolvedTheme === 'dark'; const runsWithDuration = runs.filter((r) => r.durationMs != null && r.durationMs > 0); if (runsWithDuration.length === 0) { @@ -62,7 +65,7 @@ export function WorkItemDurationChart({ runs }: WorkItemDurationChartProps) { status: run.status, model: run.model, costUsd: run.costUsd, - color: getAgentColor(run.agentType), + color: getAgentColor(run.agentType, isDark), pct: totalMs > 0 ? (durationMs / totalMs) * 100 : 0, }; }); @@ -72,7 +75,7 @@ export function WorkItemDurationChart({ runs }: WorkItemDurationChartProps) { const legendItems = uniqueAgentTypes.map((at) => ({ agentType: at, label: agentTypeLabel(at), - color: getAgentColor(at), + color: getAgentColor(at, isDark), })); return ( diff --git a/web/src/components/settings/agent-definition-shared.tsx b/web/src/components/settings/agent-definition-shared.tsx index 2bec5623..78cf2b47 100644 --- a/web/src/components/settings/agent-definition-shared.tsx +++ b/web/src/components/settings/agent-definition-shared.tsx @@ -135,7 +135,7 @@ export function Toggle({ }`} > diff --git a/web/src/index.css b/web/src/index.css index b0968a3c..4887c98b 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -4,7 +4,7 @@ @custom-variant dark (&:is(.dark *)); -@theme inline { +@theme { --color-background: oklch(1 0 0); --color-foreground: oklch(0.145 0 0); --color-card: oklch(1 0 0); diff --git a/web/src/lib/chart-colors.ts b/web/src/lib/chart-colors.ts index 99165da3..7e2436ab 100644 --- a/web/src/lib/chart-colors.ts +++ b/web/src/lib/chart-colors.ts @@ -15,7 +15,7 @@ // chart-4: oklch(0.828 0.189 84.429) ≈ #d4c02a (yellow) // chart-5: oklch(0.769 0.188 70.08) ≈ #d99c27 (amber) -// Static palette — visible in both themes, though not theme-adaptive +// Light-mode palette const CHART_PALETTE = [ '#e8642a', // chart-1: orange → planning '#3aada0', // chart-2: teal → implementation @@ -27,6 +27,24 @@ const CHART_PALETTE = [ '#2ecc71', // green → other agents ]; +// Dark-mode palette — brighter/lighter variants for visibility on dark backgrounds. +// Hex approximations of the dark-mode oklch chart colors from index.css: +// chart-1: oklch(0.488 0.243 264.376) ≈ #4d6ef5 (blue-violet) +// chart-2: oklch(0.696 0.17 162.48) ≈ #38c98a (green) +// chart-3: oklch(0.769 0.188 70.08) ≈ #e8a838 (amber) +// chart-4: oklch(0.627 0.265 303.9) ≈ #c46cf0 (violet) +// chart-5: oklch(0.645 0.246 16.439) ≈ #f0614d (red-orange) +const CHART_PALETTE_DARK = [ + '#f0844d', // orange (brighter) → planning + '#4dd6c8', // teal (brighter) → implementation + '#6fa8d0', // steel blue (brighter) → review + '#f0d44d', // yellow (brighter) → splitting + '#f0b84d', // amber (brighter) → debug + '#c084f5', // purple (brighter) → respond-to-review + '#f57070', // red (brighter) → respond-to-ci + '#4ade80', // green (brighter) → other agents +]; + const KNOWN_AGENT_TYPES: Record = { planning: 0, implementation: 1, @@ -42,18 +60,22 @@ const KNOWN_AGENT_TYPES: Record = { /** * Returns a color string for the given agent type. * Falls back to a consistent color based on the string hash for unknown types. + * + * @param agentType - The agent type identifier + * @param dark - When true, returns a brighter color suitable for dark backgrounds */ -export function getAgentColor(agentType: string): string { +export function getAgentColor(agentType: string, dark = false): string { + const palette = dark ? CHART_PALETTE_DARK : CHART_PALETTE; const idx = KNOWN_AGENT_TYPES[agentType]; if (idx !== undefined) { - return CHART_PALETTE[idx]; + return palette[idx]; } // Hash-based fallback for unknown agent types let hash = 0; for (let i = 0; i < agentType.length; i++) { - hash = (hash * 31 + agentType.charCodeAt(i)) % CHART_PALETTE.length; + hash = (hash * 31 + agentType.charCodeAt(i)) % palette.length; } - return CHART_PALETTE[Math.abs(hash) % CHART_PALETTE.length]; + return palette[Math.abs(hash) % palette.length]; } /**