Date: Sun, 12 Apr 2026 01:25:52 -0400
Subject: [PATCH 010/489] chore(guardian-r2): replace raw header in
QuickCapture with @ui Text
R1 fixes addressed the muted-text
at line 264 but missed the
"Quick Capture" label
at line 142. Replace with .
---
src/renderer/features/dashboard/components/QuickCapture.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/renderer/features/dashboard/components/QuickCapture.tsx b/src/renderer/features/dashboard/components/QuickCapture.tsx
index 7553b946..a70de28e 100644
--- a/src/renderer/features/dashboard/components/QuickCapture.tsx
+++ b/src/renderer/features/dashboard/components/QuickCapture.tsx
@@ -139,7 +139,7 @@ export function QuickCapture() {
return (
- Quick Capture
+ Quick Capture
Date: Sun, 12 Apr 2026 02:02:01 -0400
Subject: [PATCH 011/489] fix(scripts): update ESSENTIAL_SECTIONS to match
post-rewrite CLAUDE.md
The generate-worktree-claude.mjs script was extracting 0 project-rule sections
because its ESSENTIAL_SECTIONS list still referenced headings from the old
pre-rewrite CLAUDE.md (ADC v2 Refactor, ESLint Rules, Import Order, etc.).
Updated the list to match the current top-level headings: Architecture, Data
Layer, Feature Slice Design, Design System Rules, IPC Conventions, Key Paths,
Testing, Communication Standards. The extractor matches from `##
`
to the next `## ` so these top-level sections also capture their subsections.
Verified: regenerating CLAUDE.md now reports "Sections: 8 project rules extracted".
Co-Authored-By: Claude Opus 4.6 (1M context)
---
scripts/generate-worktree-claude.mjs | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/scripts/generate-worktree-claude.mjs b/scripts/generate-worktree-claude.mjs
index 0f39f1d8..5ed5dcce 100644
--- a/scripts/generate-worktree-claude.mjs
+++ b/scripts/generate-worktree-claude.mjs
@@ -211,17 +211,17 @@ const allDocs = [...new Set([...alwaysDocs, ...roleDocs])];
// ---------------------------------------------------------------------------
const ESSENTIAL_SECTIONS = [
- 'ADC v2 Refactor — Active (P0)',
- 'Verification Requirements — MANDATORY (Non-Skippable)',
- 'ESLint Rules — What You MUST Know',
- 'Import Order (Enforced)',
- 'Design System — Critical Rules',
- 'Critical Pattern: IPC Contract',
- 'Service Pattern',
- 'Feature Module Pattern',
- 'React Component Pattern',
- 'Path Aliases',
- 'State Management',
+ // Updated 2026-04-12 to match post-rewrite CLAUDE.md headings.
+ // The extractor matches from `## ` to the next `## ` or EOF, so
+ // capturing top-level sections also picks up their subsections.
+ 'Architecture',
+ 'Data Layer',
+ 'Feature Slice Design',
+ 'Design System Rules',
+ 'IPC Conventions',
+ 'Key Paths',
+ 'Testing',
+ 'Communication Standards',
];
const extractedSections = ESSENTIAL_SECTIONS
From b326261c0a02a4409b7fd3c725ba581733ecf45f Mon Sep 17 00:00:00 2001
From: ParkerES
Date: Sun, 12 Apr 2026 02:41:07 -0400
Subject: [PATCH 012/489] feat(briefing): add BriefingConfigPanel for config
CRUD (Task 2.7)
- Create BriefingConfigPanel dialog with enabled Switch, scheduledTime
Input (type=time), includeGitHub Checkbox, includeAgentActivity Checkbox
- Wire to existing useBriefingConfig + useUpdateBriefingConfig hooks
- Add gear icon button in BriefingPage header that opens the config panel
- Export BriefingConfigPanel from briefing feature barrel
Co-Authored-By: Claude Sonnet 4.6
---
.../components/BriefingConfigPanel.tsx | 172 ++++++++++++++++++
.../briefing/components/BriefingPage.tsx | 15 ++
.../features/personal/briefing/index.ts | 1 +
3 files changed, 188 insertions(+)
create mode 100644 src/renderer/features/personal/briefing/components/BriefingConfigPanel.tsx
diff --git a/src/renderer/features/personal/briefing/components/BriefingConfigPanel.tsx b/src/renderer/features/personal/briefing/components/BriefingConfigPanel.tsx
new file mode 100644
index 00000000..4a01c02d
--- /dev/null
+++ b/src/renderer/features/personal/briefing/components/BriefingConfigPanel.tsx
@@ -0,0 +1,172 @@
+/**
+ * BriefingConfigPanel — Dialog for editing daily briefing configuration.
+ * Controls: enabled toggle, scheduled time, GitHub inclusion, agent activity inclusion.
+ */
+
+import { useEffect, useState } from 'react';
+
+import { Settings } from 'lucide-react';
+
+import {
+ Button,
+ Checkbox,
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ Input,
+ Label,
+ Spinner,
+ Switch,
+} from '@ui';
+
+import { useBriefingConfig, useUpdateBriefingConfig } from '../api/useBriefing';
+
+interface BriefingConfigPanelProps {
+ open: boolean;
+ onClose: () => void;
+}
+
+export function BriefingConfigPanel({ open, onClose }: BriefingConfigPanelProps) {
+ const { data: config, isLoading } = useBriefingConfig();
+ const updateConfig = useUpdateBriefingConfig();
+
+ const [enabled, setEnabled] = useState(false);
+ const [scheduledTime, setScheduledTime] = useState('08:00');
+ const [includeGitHub, setIncludeGitHub] = useState(false);
+ const [includeAgentActivity, setIncludeAgentActivity] = useState(false);
+
+ // Sync form state when config loads or dialog opens
+ useEffect(() => {
+ if (config !== undefined) {
+ setEnabled(config.enabled);
+ setScheduledTime(config.scheduledTime);
+ setIncludeGitHub(config.includeGitHub);
+ setIncludeAgentActivity(config.includeAgentActivity);
+ }
+ }, [config, open]);
+
+ function handleSave() {
+ updateConfig.mutate(
+ { enabled, scheduledTime, includeGitHub, includeAgentActivity },
+ { onSuccess: onClose },
+ );
+ }
+
+ function handleOpenChange(isOpen: boolean) {
+ if (!isOpen) onClose();
+ }
+
+ function renderBody() {
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Enabled toggle */}
+
+
+
+
+ Automatically generate a briefing each day
+
+
+
+
+
+ {/* Scheduled time */}
+
+
+
setScheduledTime(e.target.value)}
+ />
+
+ Time when your daily briefing will be generated
+
+
+
+ {/* Include GitHub */}
+
+
setIncludeGitHub(checked === true)}
+ />
+
+
+
+ Show unread GitHub notification count in your briefing
+
+
+
+
+ {/* Include agent activity */}
+
+
setIncludeAgentActivity(checked === true)}
+ />
+
+
+
+ Show running and completed agent sessions in your briefing
+
+
+
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/src/renderer/features/personal/briefing/components/BriefingPage.tsx b/src/renderer/features/personal/briefing/components/BriefingPage.tsx
index 17f1086d..0cf80e18 100644
--- a/src/renderer/features/personal/briefing/components/BriefingPage.tsx
+++ b/src/renderer/features/personal/briefing/components/BriefingPage.tsx
@@ -2,6 +2,8 @@
* BriefingPage — Daily briefing with tasks, agents, and suggestions
*/
+import { useState } from 'react';
+
import {
AlertCircle,
CheckCircle2,
@@ -10,6 +12,7 @@ import {
GitBranch,
Lightbulb,
RefreshCw,
+ Settings,
Sun,
} from 'lucide-react';
@@ -17,6 +20,7 @@ import { Button, Card, CardContent, EmptyState, Heading, MetricCard, PageContent
import { useDailyBriefing, useGenerateBriefing, useSuggestions } from '../api/useBriefing';
+import { BriefingConfigPanel } from './BriefingConfigPanel';
import { SuggestionCard } from './SuggestionCard';
function formatTime(isoString: string): string {
@@ -36,6 +40,7 @@ export function BriefingPage() {
const { data: briefing, isLoading: briefingLoading } = useDailyBriefing();
const { data: suggestions } = useSuggestions();
const generateBriefing = useGenerateBriefing();
+ const [configOpen, setConfigOpen] = useState(false);
const displaySuggestions = briefing?.suggestions ?? suggestions ?? [];
const hasBriefing = briefing !== null && briefing !== undefined;
@@ -81,6 +86,14 @@ export function BriefingPage() {
Daily Briefing
+
+
+
+
diff --git a/src/renderer/features/personal/fitness/components/WorkoutLog.tsx b/src/renderer/features/personal/fitness/components/WorkoutLog.tsx
index a839182d..44ac8f81 100644
--- a/src/renderer/features/personal/fitness/components/WorkoutLog.tsx
+++ b/src/renderer/features/personal/fitness/components/WorkoutLog.tsx
@@ -8,6 +8,8 @@ import { Pencil, Trash2 } from 'lucide-react';
import type { Workout } from '@shared/types';
+import { RelativeTime } from '@renderer/shared/components/RelativeTime';
+
import { Badge, Button, EmptyState } from '@ui';
import { useDeleteWorkout, useWorkouts } from '../api/useFitness';
@@ -97,6 +99,9 @@ function WorkoutItem({ workout, onDelete }: WorkoutItemProps) {
{workout.notes ? (
{workout.notes}
) : null}
+
+
+
{new Date(note.updatedAt).toLocaleDateString()}
+
{note.tags.length > 0 ? (
{note.tags.slice(0, 3).map((tag) => (
diff --git a/src/renderer/features/roadmap/components/RoadmapPage.tsx b/src/renderer/features/roadmap/components/RoadmapPage.tsx
index c937dda3..3affa347 100644
--- a/src/renderer/features/roadmap/components/RoadmapPage.tsx
+++ b/src/renderer/features/roadmap/components/RoadmapPage.tsx
@@ -4,6 +4,7 @@ import { CheckCircle2, Circle, Clock, Map, Pencil, Plus, Sparkles, Square, Squar
import type { Milestone, MilestoneStatus } from '@shared/types';
+import { RelativeTime } from '@renderer/shared/components/RelativeTime';
import { cn } from '@renderer/shared/lib/utils';
import { useAssistantWidgetStore, useLayoutStore } from '@renderer/shared/stores';
@@ -144,6 +145,9 @@ function MilestoneCard({
Target: {new Date(milestone.targetDate).toLocaleDateString()}
+
+
+
{/* Progress Bar */}
diff --git a/src/renderer/features/tasks/components/grid/ProgressTaskGrid.tsx b/src/renderer/features/tasks/components/grid/ProgressTaskGrid.tsx
index fcba1819..f2d3b4b7 100644
--- a/src/renderer/features/tasks/components/grid/ProgressTaskGrid.tsx
+++ b/src/renderer/features/tasks/components/grid/ProgressTaskGrid.tsx
@@ -18,6 +18,7 @@ import { Archive, ArrowUpDown, ChevronDown, ChevronRight, Pencil, Play, Plus } f
import type { ProgressPriority, ProgressStatus, ProgressTask } from '@shared/types/progress';
+import { RelativeTime } from '@renderer/shared/components/RelativeTime';
import { cn, formatRelativeTime } from '@renderer/shared/lib/utils';
import {
@@ -529,6 +530,23 @@ function createProgressColumns(
);
},
},
+ {
+ accessorKey: 'createdAt',
+ header: ({ column }) => (
+
column.toggleSorting(column.getIsSorted() === 'asc')}
+ >
+ Created
+
+
+ ),
+ size: 110,
+ cell: ({ row }) => (
+
('createdAt')} />
+ ),
+ },
{
accessorKey: 'updatedAt',
header: ({ column }) => (
diff --git a/src/renderer/shared/components/RelativeTime.tsx b/src/renderer/shared/components/RelativeTime.tsx
new file mode 100644
index 00000000..b2d42682
--- /dev/null
+++ b/src/renderer/shared/components/RelativeTime.tsx
@@ -0,0 +1,30 @@
+/**
+ * RelativeTime — shared component displaying a relative timestamp (e.g. "2h ago")
+ * with a tooltip showing the full absolute ISO date/time on hover.
+ */
+
+import { formatRelativeTime } from '@renderer/shared/lib/utils';
+
+import { Tooltip, TooltipContent, TooltipTrigger } from '@ui';
+
+interface RelativeTimeProps {
+ value: string | null | undefined;
+ className?: string;
+}
+
+export function RelativeTime({ value, className }: RelativeTimeProps) {
+ if (value === null || value === undefined) {
+ return —;
+ }
+
+ const relative = formatRelativeTime(value);
+
+ return (
+
+
+ {relative}
+
+ {value}
+
+ );
+}
From 86e2defbda135a55e8cc96418688e23d405b6fe7 Mon Sep 17 00:00:00 2001
From: ParkerES
Date: Sun, 12 Apr 2026 05:12:18 -0400
Subject: [PATCH 022/489] fix(guardian-r1): resolve 8 design-system + file-size
violations
---
src/main/features/fitness/fitness-service.ts | 38 +----
.../features/ideation/components/IdeaCard.tsx | 6 +-
.../ideation/components/IdeaEditForm.tsx | 33 +---
.../components/BriefingConfigPanel.tsx | 17 +-
.../changelog/components/VersionCard.tsx | 3 +-
.../personal/fitness/api/useFitness.ts | 153 ++----------------
.../fitness/api/useFitnessMutations.ts | 147 +++++++++++++++++
.../fitness/components/BodyComposition.tsx | 19 +--
.../fitness/components/WorkoutEditDialog.tsx | 124 ++------------
.../components/WorkoutExerciseList.tsx | 145 +++++++++++++++++
.../fitness/components/WorkoutLog.tsx | 4 +-
.../features/personal/fitness/index.ts | 15 +-
12 files changed, 362 insertions(+), 342 deletions(-)
create mode 100644 src/renderer/features/personal/fitness/api/useFitnessMutations.ts
create mode 100644 src/renderer/features/personal/fitness/components/WorkoutExerciseList.tsx
diff --git a/src/main/features/fitness/fitness-service.ts b/src/main/features/fitness/fitness-service.ts
index fb05f6af..b6511404 100644
--- a/src/main/features/fitness/fitness-service.ts
+++ b/src/main/features/fitness/fitness-service.ts
@@ -110,38 +110,24 @@ interface StoreData {
function migrateFromJson(db: AdcDatabase, dataDir: string): void {
const fitnessDir = join(dataDir, 'fitness');
-
- // ── Workouts ──
migrateWorkouts(db, fitnessDir);
-
- // ── Measurements ──
migrateMeasurements(db, fitnessDir);
-
- // ── Goals ──
migrateGoals(db, fitnessDir);
}
function migrateWorkouts(db: AdcDatabase, fitnessDir: string): void {
const existing = db.select().from(workouts).limit(1).all();
if (existing.length > 0) return;
-
const jsonPath = join(fitnessDir, 'workouts.json');
if (!existsSync(jsonPath)) return;
-
try {
const raw = readFileSync(jsonPath, 'utf-8');
const parsed = JSON.parse(raw) as Partial>;
const items = Array.isArray(parsed.items) ? parsed.items : [];
-
for (const item of items) {
db.insert(workouts).values({
- id: item.id,
- date: item.date,
- type: item.type,
- duration: item.duration,
- exercises: item.exercises as unknown[],
- notes: item.notes ?? null,
- createdAt: item.createdAt,
+ id: item.id, date: item.date, type: item.type, duration: item.duration,
+ exercises: item.exercises as unknown[], notes: item.notes ?? null, createdAt: item.createdAt,
}).run();
}
logger.info(`Migrated ${String(items.length)} workouts from JSON to SQLite`);
@@ -153,27 +139,21 @@ function migrateWorkouts(db: AdcDatabase, fitnessDir: string): void {
function migrateMeasurements(db: AdcDatabase, fitnessDir: string): void {
const existing = db.select().from(bodyMeasurements).limit(1).all();
if (existing.length > 0) return;
-
const jsonPath = join(fitnessDir, 'measurements.json');
if (!existsSync(jsonPath)) return;
-
try {
const raw = readFileSync(jsonPath, 'utf-8');
const parsed = JSON.parse(raw) as Partial>;
const items = Array.isArray(parsed.items) ? parsed.items : [];
-
for (const item of items) {
db.insert(bodyMeasurements).values({
- id: item.id,
- date: item.date,
+ id: item.id, date: item.date, source: item.source, createdAt: item.createdAt,
weight: item.weight === undefined ? null : Math.round(item.weight),
bodyFat: item.bodyFat === undefined ? null : Math.round(item.bodyFat),
muscleMass: item.muscleMass === undefined ? null : Math.round(item.muscleMass),
boneMass: item.boneMass === undefined ? null : Math.round(item.boneMass),
waterPercentage: item.waterPercentage === undefined ? null : Math.round(item.waterPercentage),
visceralFat: item.visceralFat === undefined ? null : Math.round(item.visceralFat),
- source: item.source,
- createdAt: item.createdAt,
}).run();
}
logger.info(`Migrated ${String(items.length)} measurements from JSON to SQLite`);
@@ -185,24 +165,16 @@ function migrateMeasurements(db: AdcDatabase, fitnessDir: string): void {
function migrateGoals(db: AdcDatabase, fitnessDir: string): void {
const existing = db.select().from(fitnessGoals).limit(1).all();
if (existing.length > 0) return;
-
const jsonPath = join(fitnessDir, 'goals.json');
if (!existsSync(jsonPath)) return;
-
try {
const raw = readFileSync(jsonPath, 'utf-8');
const parsed = JSON.parse(raw) as Partial>;
const items = Array.isArray(parsed.items) ? parsed.items : [];
-
for (const item of items) {
db.insert(fitnessGoals).values({
- id: item.id,
- type: item.type,
- target: Math.round(item.target),
- current: Math.round(item.current),
- unit: item.unit,
- deadline: item.deadline ?? null,
- createdAt: item.createdAt,
+ id: item.id, type: item.type, unit: item.unit, deadline: item.deadline ?? null,
+ target: Math.round(item.target), current: Math.round(item.current), createdAt: item.createdAt,
}).run();
}
logger.info(`Migrated ${String(items.length)} fitness goals from JSON to SQLite`);
diff --git a/src/renderer/features/ideation/components/IdeaCard.tsx b/src/renderer/features/ideation/components/IdeaCard.tsx
index 9da41a16..dd57fe65 100644
--- a/src/renderer/features/ideation/components/IdeaCard.tsx
+++ b/src/renderer/features/ideation/components/IdeaCard.tsx
@@ -9,7 +9,7 @@ import type { Idea, IdeaCategory } from '@shared/types';
import { RelativeTime } from '@renderer/shared/components/RelativeTime';
import { cn } from '@renderer/shared/lib/utils';
-import { Badge, Button, Card, CardContent } from '@ui';
+import { Badge, Button, Card, CardContent, Text } from '@ui';
const CATEGORY_CONFIG: Record = {
feature: { label: 'Feature', colorClass: 'text-primary' },
@@ -66,9 +66,9 @@ export function IdeaCard({ idea, onDelete, onEdit, onVote }: IdeaCardProps) {
{/* Title & Description */}
{idea.title}
-
+
{idea.description}
-
+
{/* Tags */}
{hasTags ? (
diff --git a/src/renderer/features/ideation/components/IdeaEditForm.tsx b/src/renderer/features/ideation/components/IdeaEditForm.tsx
index 5ab78b75..2db84062 100644
--- a/src/renderer/features/ideation/components/IdeaEditForm.tsx
+++ b/src/renderer/features/ideation/components/IdeaEditForm.tsx
@@ -30,35 +30,10 @@ import {
import { useUpdateIdea } from '../api/useIdeas';
-const CATEGORY_OPTIONS: readonly IdeaCategory[] = [
- 'feature',
- 'improvement',
- 'bug',
- 'performance',
-];
-
-const STATUS_OPTIONS: readonly IdeaStatus[] = [
- 'new',
- 'exploring',
- 'accepted',
- 'rejected',
- 'implemented',
-];
-
-const CATEGORY_LABELS: Record = {
- feature: 'Feature',
- improvement: 'Improvement',
- bug: 'Bug',
- performance: 'Performance',
-};
-
-const STATUS_LABELS: Record = {
- new: 'New',
- exploring: 'Exploring',
- accepted: 'Accepted',
- rejected: 'Rejected',
- implemented: 'Implemented',
-};
+const CATEGORY_OPTIONS: readonly IdeaCategory[] = ['feature', 'improvement', 'bug', 'performance'];
+const STATUS_OPTIONS: readonly IdeaStatus[] = ['new', 'exploring', 'accepted', 'rejected', 'implemented'];
+const CATEGORY_LABELS: Record = { feature: 'Feature', improvement: 'Improvement', bug: 'Bug', performance: 'Performance' };
+const STATUS_LABELS: Record = { new: 'New', exploring: 'Exploring', accepted: 'Accepted', rejected: 'Rejected', implemented: 'Implemented' };
interface IdeaEditFormProps {
idea: Idea | null;
diff --git a/src/renderer/features/personal/briefing/components/BriefingConfigPanel.tsx b/src/renderer/features/personal/briefing/components/BriefingConfigPanel.tsx
index 4a01c02d..d77a3e7a 100644
--- a/src/renderer/features/personal/briefing/components/BriefingConfigPanel.tsx
+++ b/src/renderer/features/personal/briefing/components/BriefingConfigPanel.tsx
@@ -19,6 +19,7 @@ import {
Label,
Spinner,
Switch,
+ Text,
} from '@ui';
import { useBriefingConfig, useUpdateBriefingConfig } from '../api/useBriefing';
@@ -75,9 +76,9 @@ export function BriefingConfigPanel({ open, onClose }: BriefingConfigPanelProps)
-
+
Automatically generate a briefing each day
-
+
setScheduledTime(e.target.value)}
/>
-
+
Time when your daily briefing will be generated
-
+
{/* Include GitHub */}
@@ -115,9 +116,9 @@ export function BriefingConfigPanel({ open, onClose }: BriefingConfigPanelProps)
-
+
Show unread GitHub notification count in your briefing
-
+
@@ -133,9 +134,9 @@ export function BriefingConfigPanel({ open, onClose }: BriefingConfigPanelProps)
-
+
Show running and completed agent sessions in your briefing
-
+
diff --git a/src/renderer/features/personal/changelog/components/VersionCard.tsx b/src/renderer/features/personal/changelog/components/VersionCard.tsx
index 09c599d4..77da8568 100644
--- a/src/renderer/features/personal/changelog/components/VersionCard.tsx
+++ b/src/renderer/features/personal/changelog/components/VersionCard.tsx
@@ -22,6 +22,7 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
+ Text,
} from '@ui';
import { useDeleteChangelogEntry } from '../api/useChangelog';
@@ -89,7 +90,7 @@ export function VersionCard({ entry }: VersionCardProps) {
))
) : (
-
No changes listed.
+ No changes listed.
)}
diff --git a/src/renderer/features/personal/fitness/api/useFitness.ts b/src/renderer/features/personal/fitness/api/useFitness.ts
index 705a4f6a..bc10b9d1 100644
--- a/src/renderer/features/personal/fitness/api/useFitness.ts
+++ b/src/renderer/features/personal/fitness/api/useFitness.ts
@@ -1,23 +1,28 @@
/**
- * React Query hooks for fitness
+ * React Query hooks for fitness — query hooks
+ * Mutation hooks live in useFitnessMutations.ts
*/
-import { type UseMutationResult, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { FITNESS } from '@shared/ipc/fitness/channels';
-import type {
- BodyMeasurement,
- Exercise,
- FitnessGoal,
- FitnessGoalType,
- MeasurementSource,
- WorkoutType,
-} from '@shared/types';
+import type { FitnessGoalType, WorkoutType } from '@shared/types';
import { ipc } from '@renderer/shared/lib/ipc';
import { fitnessKeys } from './queryKeys';
+// Re-export mutation hooks so existing imports continue to resolve
+export {
+ useDeleteMeasurement,
+ useLogMeasurement,
+ useLogWorkout,
+ useUpdateGoal,
+ useUpdateGoalProgress,
+ useUpdateMeasurement,
+ useUpdateWorkout,
+} from './useFitnessMutations';
+
/** List workouts with optional filters */
export function useWorkouts(filters?: {
startDate?: string;
@@ -30,45 +35,6 @@ export function useWorkouts(filters?: {
});
}
-/** Log a new workout */
-export function useLogWorkout() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (data: {
- date: string;
- type: WorkoutType;
- duration: number;
- exercises: Exercise[];
- notes?: string;
- id?: string;
- }) => {
- const id = data.id ?? crypto.randomUUID();
- return ipc(FITNESS.LOG.WORKOUT, { ...data, id });
- },
- onSuccess() {
- void queryClient.invalidateQueries({ queryKey: fitnessKeys.workouts() });
- },
- });
-}
-
-/** Update an existing workout */
-export function useUpdateWorkout() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (data: {
- id: string;
- date?: string;
- type?: WorkoutType;
- duration?: number;
- exercises?: Exercise[];
- notes?: string;
- }) => ipc(FITNESS.UPDATE.WORKOUT, data),
- onSuccess() {
- void queryClient.invalidateQueries({ queryKey: fitnessKeys.workouts() });
- },
- });
-}
-
/** Delete a workout */
export function useDeleteWorkout() {
const queryClient = useQueryClient();
@@ -80,40 +46,6 @@ export function useDeleteWorkout() {
});
}
-interface UpdateMeasurementInput {
- id: string;
- date?: string;
- weight?: number;
- bodyFat?: number;
- muscleMass?: number;
- boneMass?: number;
- waterPercentage?: number;
- visceralFat?: number;
- source?: MeasurementSource;
-}
-
-/** Update an existing measurement */
-export function useUpdateMeasurement(): UseMutationResult {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (data: UpdateMeasurementInput) => ipc(FITNESS.UPDATE.MEASUREMENT, data),
- onSuccess() {
- void queryClient.invalidateQueries({ queryKey: fitnessKeys.measurements() });
- },
- });
-}
-
-/** Delete a measurement */
-export function useDeleteMeasurement(): UseMutationResult<{ success: boolean }, Error, string> {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (id: string) => ipc(FITNESS.DELETE.MEASUREMENT, { id }),
- onSuccess() {
- void queryClient.invalidateQueries({ queryKey: fitnessKeys.measurements() });
- },
- });
-}
-
/** Get body measurements */
export function useMeasurements(limit?: number) {
return useQuery({
@@ -122,30 +54,6 @@ export function useMeasurements(limit?: number) {
});
}
-/** Log a body measurement */
-export function useLogMeasurement() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (data: {
- date: string;
- weight?: number;
- bodyFat?: number;
- muscleMass?: number;
- boneMass?: number;
- waterPercentage?: number;
- visceralFat?: number;
- source: MeasurementSource;
- id?: string;
- }) => {
- const id = data.id ?? crypto.randomUUID();
- return ipc(FITNESS.LOG.MEASUREMENT, { ...data, id });
- },
- onSuccess() {
- void queryClient.invalidateQueries({ queryKey: fitnessKeys.measurements() });
- },
- });
-}
-
/** Get fitness stats */
export function useFitnessStats() {
return useQuery({
@@ -182,18 +90,6 @@ export function useSetGoal() {
});
}
-/** Update goal progress */
-export function useUpdateGoalProgress() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (data: { goalId: string; current: number }) =>
- ipc(FITNESS.UPDATE['GOAL-PROGRESS'], data),
- onSuccess: () => {
- void queryClient.invalidateQueries({ queryKey: fitnessKeys.goals() });
- },
- });
-}
-
/** Delete a goal */
export function useDeleteGoal() {
const queryClient = useQueryClient();
@@ -204,22 +100,3 @@ export function useDeleteGoal() {
},
});
}
-
-interface UpdateGoalInput {
- id: string;
- type?: FitnessGoalType;
- target?: number;
- unit?: string;
- deadline?: string | null;
-}
-
-/** Update an existing goal's definition (type, target, unit, deadline) */
-export function useUpdateGoal(): UseMutationResult {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (data: UpdateGoalInput) => ipc(FITNESS.UPDATE.GOAL, data),
- onSuccess() {
- void queryClient.invalidateQueries({ queryKey: fitnessKeys.goals() });
- },
- });
-}
diff --git a/src/renderer/features/personal/fitness/api/useFitnessMutations.ts b/src/renderer/features/personal/fitness/api/useFitnessMutations.ts
new file mode 100644
index 00000000..fb9a2ef1
--- /dev/null
+++ b/src/renderer/features/personal/fitness/api/useFitnessMutations.ts
@@ -0,0 +1,147 @@
+/**
+ * React Query mutation hooks for fitness
+ */
+
+import { type UseMutationResult, useMutation, useQueryClient } from '@tanstack/react-query';
+
+import { FITNESS } from '@shared/ipc/fitness/channels';
+import type {
+ BodyMeasurement,
+ Exercise,
+ FitnessGoal,
+ FitnessGoalType,
+ MeasurementSource,
+ WorkoutType,
+} from '@shared/types';
+
+import { ipc } from '@renderer/shared/lib/ipc';
+
+import { fitnessKeys } from './queryKeys';
+
+/** Log a new workout */
+export function useLogWorkout() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (data: {
+ date: string;
+ type: WorkoutType;
+ duration: number;
+ exercises: Exercise[];
+ notes?: string;
+ id?: string;
+ }) => {
+ const id = data.id ?? crypto.randomUUID();
+ return ipc(FITNESS.LOG.WORKOUT, { ...data, id });
+ },
+ onSuccess() {
+ void queryClient.invalidateQueries({ queryKey: fitnessKeys.workouts() });
+ },
+ });
+}
+
+/** Update an existing workout */
+export function useUpdateWorkout() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (data: {
+ id: string;
+ date?: string;
+ type?: WorkoutType;
+ duration?: number;
+ exercises?: Exercise[];
+ notes?: string;
+ }) => ipc(FITNESS.UPDATE.WORKOUT, data),
+ onSuccess() {
+ void queryClient.invalidateQueries({ queryKey: fitnessKeys.workouts() });
+ },
+ });
+}
+
+interface UpdateMeasurementInput {
+ id: string;
+ date?: string;
+ weight?: number;
+ bodyFat?: number;
+ muscleMass?: number;
+ boneMass?: number;
+ waterPercentage?: number;
+ visceralFat?: number;
+ source?: MeasurementSource;
+}
+
+/** Update an existing measurement */
+export function useUpdateMeasurement(): UseMutationResult {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (data: UpdateMeasurementInput) => ipc(FITNESS.UPDATE.MEASUREMENT, data),
+ onSuccess() {
+ void queryClient.invalidateQueries({ queryKey: fitnessKeys.measurements() });
+ },
+ });
+}
+
+/** Delete a measurement */
+export function useDeleteMeasurement(): UseMutationResult<{ success: boolean }, Error, string> {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (id: string) => ipc(FITNESS.DELETE.MEASUREMENT, { id }),
+ onSuccess() {
+ void queryClient.invalidateQueries({ queryKey: fitnessKeys.measurements() });
+ },
+ });
+}
+
+/** Log a body measurement */
+export function useLogMeasurement() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (data: {
+ date: string;
+ weight?: number;
+ bodyFat?: number;
+ muscleMass?: number;
+ boneMass?: number;
+ waterPercentage?: number;
+ visceralFat?: number;
+ source: MeasurementSource;
+ id?: string;
+ }) => {
+ const id = data.id ?? crypto.randomUUID();
+ return ipc(FITNESS.LOG.MEASUREMENT, { ...data, id });
+ },
+ onSuccess() {
+ void queryClient.invalidateQueries({ queryKey: fitnessKeys.measurements() });
+ },
+ });
+}
+
+interface UpdateGoalInput {
+ id: string;
+ type?: FitnessGoalType;
+ target?: number;
+ unit?: string;
+ deadline?: string | null;
+}
+
+/** Update an existing goal's definition (type, target, unit, deadline) */
+export function useUpdateGoal(): UseMutationResult {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (data: UpdateGoalInput) => ipc(FITNESS.UPDATE.GOAL, data),
+ onSuccess() {
+ void queryClient.invalidateQueries({ queryKey: fitnessKeys.goals() });
+ },
+ });
+}
+
+/** Update goal progress */
+export function useUpdateGoalProgress() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (data: { goalId: string; current: number }) =>
+ ipc(FITNESS.UPDATE['GOAL-PROGRESS'], data),
+ onSuccess: () => {
+ void queryClient.invalidateQueries({ queryKey: fitnessKeys.goals() });
+ },
+ });
+}
diff --git a/src/renderer/features/personal/fitness/components/BodyComposition.tsx b/src/renderer/features/personal/fitness/components/BodyComposition.tsx
index 2476444b..fc084f21 100644
--- a/src/renderer/features/personal/fitness/components/BodyComposition.tsx
+++ b/src/renderer/features/personal/fitness/components/BodyComposition.tsx
@@ -25,6 +25,7 @@ import {
EmptyState,
Input,
Label,
+ Text,
} from '@ui';
import { useDeleteMeasurement, useLogMeasurement, useMeasurements } from '../api/useFitness';
@@ -79,45 +80,45 @@ export function BodyComposition() {
{latest.weight === undefined ? null : (
Weight
-
{String(latest.weight)} kg
+
{String(latest.weight)} kg
)}
{latest.bodyFat === undefined ? null : (
Body Fat
-
{String(latest.bodyFat)}%
+
{String(latest.bodyFat)}%
)}
{latest.muscleMass === undefined ? null : (
Muscle Mass
-
{String(latest.muscleMass)} kg
+
{String(latest.muscleMass)} kg
)}
{latest.boneMass === undefined ? null : (
Bone Mass
-
{String(latest.boneMass)} kg
+
{String(latest.boneMass)} kg
)}
{latest.waterPercentage === undefined ? null : (
Water
-
+
{String(latest.waterPercentage)}%
-
+
)}
{latest.visceralFat === undefined ? null : (
Visceral Fat
-
{String(latest.visceralFat)}
+
{String(latest.visceralFat)}
)}
-
+
{latest.date} · {latest.source}
-
+
) : (
diff --git a/src/renderer/features/personal/fitness/components/WorkoutEditDialog.tsx b/src/renderer/features/personal/fitness/components/WorkoutEditDialog.tsx
index 2c8e2ac3..10d2f1e6 100644
--- a/src/renderer/features/personal/fitness/components/WorkoutEditDialog.tsx
+++ b/src/renderer/features/personal/fitness/components/WorkoutEditDialog.tsx
@@ -4,8 +4,6 @@
import { useState } from 'react';
-import { Plus, Trash2 } from 'lucide-react';
-
import type { Exercise, ExerciseSet, Workout, WorkoutType } from '@shared/types';
import {
@@ -27,6 +25,8 @@ import {
import { useUpdateWorkout } from '../api/useFitness';
+import { WorkoutExerciseList } from './WorkoutExerciseList';
+
// ── Helpers ─────────────────────────────────────────────────
let nextKey = 0;
@@ -35,7 +35,7 @@ function uid(): string {
return `ek-${String(nextKey)}`;
}
-interface FormExercise extends Exercise {
+export interface FormExercise extends Exercise {
_key: string;
_setKeys: string[];
}
@@ -205,36 +205,14 @@ export function WorkoutEditDialog({ workout, open, onOpenChange }: WorkoutEditDi
{/* Exercises */}
-
-
-
Exercises
-
-
- Add Exercise
-
-
-
- {exercises.map((exercise, exerciseIndex) => (
- handleAddSet(exerciseIndex)}
- onNameChange={(name) => handleExerciseNameChange(exerciseIndex, name)}
- onRemove={() => handleRemoveExercise(exerciseIndex)}
- onSetChange={(setIndex, field, value) =>
- handleSetChange(exerciseIndex, setIndex, field, value)
- }
- />
- ))}
-
-
+
{/* Notes */}
@@ -276,83 +254,3 @@ export function WorkoutEditDialog({ workout, open, onOpenChange }: WorkoutEditDi
);
}
-// ── EditExerciseInput ─────────────────────────────────────────
-
-interface EditExerciseInputProps {
- exercise: FormExercise;
- exerciseIndex: number;
- onNameChange: (name: string) => void;
- onRemove: () => void;
- onAddSet: () => void;
- onSetChange: (setIndex: number, field: keyof ExerciseSet, value: string) => void;
-}
-
-function EditExerciseInput({
- exercise,
- exerciseIndex,
- onNameChange,
- onRemove,
- onAddSet,
- onSetChange,
-}: EditExerciseInputProps) {
- return (
-
-
- onNameChange(e.target.value)}
- />
-
-
-
-
-
-
- + Add Set
-
-
- );
-}
diff --git a/src/renderer/features/personal/fitness/components/WorkoutExerciseList.tsx b/src/renderer/features/personal/fitness/components/WorkoutExerciseList.tsx
new file mode 100644
index 00000000..7cf506d2
--- /dev/null
+++ b/src/renderer/features/personal/fitness/components/WorkoutExerciseList.tsx
@@ -0,0 +1,145 @@
+/**
+ * WorkoutExerciseList — Exercise list editor sub-component for WorkoutEditDialog
+ */
+
+import { Plus, Trash2 } from 'lucide-react';
+
+import type { ExerciseSet } from '@shared/types';
+
+import { Button, Input } from '@ui';
+
+import type { FormExercise } from './WorkoutEditDialog';
+
+// ── EditExerciseInput ─────────────────────────────────────────
+
+interface EditExerciseInputProps {
+ exercise: FormExercise;
+ exerciseIndex: number;
+ onNameChange: (name: string) => void;
+ onRemove: () => void;
+ onAddSet: () => void;
+ onSetChange: (setIndex: number, field: keyof ExerciseSet, value: string) => void;
+}
+
+function EditExerciseInput({
+ exercise,
+ exerciseIndex,
+ onNameChange,
+ onRemove,
+ onAddSet,
+ onSetChange,
+}: EditExerciseInputProps) {
+ return (
+
+
+ onNameChange(e.target.value)}
+ />
+
+
+
+
+
+
+ + Add Set
+
+
+ );
+}
+
+// ── WorkoutExerciseList ───────────────────────────────────────
+
+interface WorkoutExerciseListProps {
+ exercises: FormExercise[];
+ onAddExercise: () => void;
+ onRemoveExercise: (index: number) => void;
+ onExerciseNameChange: (index: number, name: string) => void;
+ onAddSet: (exerciseIndex: number) => void;
+ onSetChange: (exerciseIndex: number, setIndex: number, field: keyof ExerciseSet, value: string) => void;
+}
+
+export function WorkoutExerciseList({
+ exercises,
+ onAddExercise,
+ onRemoveExercise,
+ onExerciseNameChange,
+ onAddSet,
+ onSetChange,
+}: WorkoutExerciseListProps) {
+ return (
+
+
+
Exercises
+
+
+ Add Exercise
+
+
+
+ {exercises.map((exercise, exerciseIndex) => (
+ onAddSet(exerciseIndex)}
+ onNameChange={(name) => onExerciseNameChange(exerciseIndex, name)}
+ onRemove={() => onRemoveExercise(exerciseIndex)}
+ onSetChange={(setIndex, field, value) =>
+ onSetChange(exerciseIndex, setIndex, field, value)
+ }
+ />
+ ))}
+
+
+ );
+}
diff --git a/src/renderer/features/personal/fitness/components/WorkoutLog.tsx b/src/renderer/features/personal/fitness/components/WorkoutLog.tsx
index 44ac8f81..a6731e46 100644
--- a/src/renderer/features/personal/fitness/components/WorkoutLog.tsx
+++ b/src/renderer/features/personal/fitness/components/WorkoutLog.tsx
@@ -10,7 +10,7 @@ import type { Workout } from '@shared/types';
import { RelativeTime } from '@renderer/shared/components/RelativeTime';
-import { Badge, Button, EmptyState } from '@ui';
+import { Badge, Button, EmptyState, Text } from '@ui';
import { useDeleteWorkout, useWorkouts } from '../api/useFitness';
@@ -97,7 +97,7 @@ function WorkoutItem({ workout, onDelete }: WorkoutItemProps) {
) : null}
{workout.notes ? (
-
{workout.notes}
+
{workout.notes}
) : null}
diff --git a/src/renderer/features/personal/fitness/index.ts b/src/renderer/features/personal/fitness/index.ts
index 0cf16884..03f28c0f 100644
--- a/src/renderer/features/personal/fitness/index.ts
+++ b/src/renderer/features/personal/fitness/index.ts
@@ -2,23 +2,26 @@
* Fitness feature — public API
*/
-// API hooks
+// API hooks — queries
export {
useDeleteGoal,
- useDeleteMeasurement,
useDeleteWorkout,
useFitnessGoals,
useFitnessStats,
- useLogMeasurement,
- useLogWorkout,
useMeasurements,
useSetGoal,
+ useWorkouts,
+} from './api/useFitness';
+// API hooks — mutations
+export {
+ useDeleteMeasurement,
+ useLogMeasurement,
+ useLogWorkout,
useUpdateGoal,
useUpdateGoalProgress,
useUpdateMeasurement,
useUpdateWorkout,
- useWorkouts,
-} from './api/useFitness';
+} from './api/useFitnessMutations';
export { fitnessKeys } from './api/queryKeys';
// Event hook
From 5ddfdbd23b596743c34df19746dc57af608ee597 Mon Sep 17 00:00:00 2001
From: ParkerES
Date: Sun, 12 Apr 2026 05:14:58 -0400
Subject: [PATCH 023/489] fix(guardian-r2): replace remaining raw tags in
WorkoutLog with Text
---
.../features/personal/fitness/components/WorkoutLog.tsx | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/renderer/features/personal/fitness/components/WorkoutLog.tsx b/src/renderer/features/personal/fitness/components/WorkoutLog.tsx
index a6731e46..21c59a15 100644
--- a/src/renderer/features/personal/fitness/components/WorkoutLog.tsx
+++ b/src/renderer/features/personal/fitness/components/WorkoutLog.tsx
@@ -86,15 +86,15 @@ function WorkoutItem({ workout, onDelete }: WorkoutItemProps) {
{workout.date}
-
+
{String(exerciseCount)} exercise{exerciseCount === 1 ? '' : 's'} ·{' '}
{String(totalSets)} set{totalSets === 1 ? '' : 's'} · {String(workout.duration)}{' '}
min
-
+
{(workout.exercises.length > 0) ? (
-
+
{workout.exercises.map((e) => e.name).join(', ')}
-
+
) : null}
{workout.notes ? (
{workout.notes}
From 0d41facf12ea923a5629b95071ab6ff3c8474331 Mon Sep 17 00:00:00 2001
From: ParkerES
Date: Sun, 12 Apr 2026 05:15:22 -0400
Subject: [PATCH 024/489] fix(guardian-r2): Text size=xs not supported, use
text-xs className
---
.../features/personal/fitness/components/WorkoutLog.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/renderer/features/personal/fitness/components/WorkoutLog.tsx b/src/renderer/features/personal/fitness/components/WorkoutLog.tsx
index 21c59a15..66ba126c 100644
--- a/src/renderer/features/personal/fitness/components/WorkoutLog.tsx
+++ b/src/renderer/features/personal/fitness/components/WorkoutLog.tsx
@@ -92,7 +92,7 @@ function WorkoutItem({ workout, onDelete }: WorkoutItemProps) {
min
{(workout.exercises.length > 0) ? (
-
+
{workout.exercises.map((e) => e.name).join(', ')}
) : null}
From 4aab248615b1d2dc2947c0b74960922623455069 Mon Sep 17 00:00:00 2001
From: Parker Manning <67211514+ParkerM2@users.noreply.github.com>
Date: Sun, 12 Apr 2026 21:47:23 -0400
Subject: [PATCH 025/489] Sprint 3: search, filter, bulk operations (#115)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat(hooks): add useDebounce shared hook
Co-Authored-By: Claude Sonnet 4.6
* feat(agent-dashboard): add session filters + search bar
Add a filter bar to the Agents tab with four controls:
- Session-type Select (all | project-owner | team-lead | teammate)
- Status Select (all | running | completed | failed | idle)
- Team-name Select populated from distinct teamName values in session list
- Search Input (debounced 250ms) filtering by session name
All filtering is client-side. @ui primitives used throughout.
Co-Authored-By: Claude Sonnet 4.6
* feat(fitness): add search + date-range filter across fitness panels
- WorkoutLog: debounced search filters by type and notes
- GoalsPanel: debounced search filters by goal type; fix raw ->
- BodyComposition: date-range (from/to) filter on measurements
- All panels render EmptyState when filters yield zero results
- All UI uses @ui primitives (SearchInput, Input, Label, Button, Text)
Co-Authored-By: Claude Sonnet 4.6
* feat(roadmap): add milestones search, status filter, and sort
* feat(alerts): add search, type filter, and sort to alerts page
* feat(progress-tasks): add bulk operations via BulkActionBar
Wire selection checkboxes in ProgressTaskGrid into a BulkActionBar that
appears when one or more tasks are selected. Supports Archive (with
AlertDialog confirmation), Delete (with AlertDialog confirmation), Change
Status, and Change Priority. Batches mutations via Promise.all over
existing useProgressMutations hooks.
Co-Authored-By: Claude Sonnet 4.6
* feat(my-work): add search, sort, priority/Jira/PR badges, and row navigation to My Work page
- Debounced search input filters tasks by title and description (250ms)
- Priority badge on each task row (Badge variant mapped from priority)
- Jira link badge (if jiraTicket/jiraUrl present) — opens external with noopener,noreferrer
- PR link badge (if prNumber/prUrl present) — opens external with noopener,noreferrer
- Sort dropdown: priority desc | updatedAt desc | status asc
- Clicking a task row navigates to /projects/$projectId/tasks using activeProjectId
- All UI uses @ui primitives only — no raw HTML
Co-Authored-By: Claude Sonnet 4.6
* fix(guardian-r1): migrate raw / to @ui Text/Heading
Co-Authored-By: Claude Sonnet 4.6
* fix(sprint-3): auto-fix jsx-sort-props warnings in AlertsPage and RoadmapPage
Co-Authored-By: Claude Sonnet 4.6
---------
Co-authored-by: ParkerES
Co-authored-by: Claude Sonnet 4.6
---
.../components/AgentDashboardPage.tsx | 217 +++++++++++--
.../my-work/components/MyWorkPage.tsx | 304 ++++++++++++++++--
.../personal/alerts/components/AlertsPage.tsx | 103 +++++-
.../fitness/components/BodyComposition.tsx | 62 +++-
.../fitness/components/GoalsPanel.tsx | 34 +-
.../fitness/components/WorkoutLog.tsx | 54 ++--
.../roadmap/components/RoadmapPage.tsx | 120 +++++--
.../tasks/components/BulkActionBar.tsx | 241 ++++++++++++++
.../components/grid/ProgressTaskGrid.tsx | 63 ++++
src/renderer/shared/hooks/index.ts | 1 +
src/renderer/shared/hooks/useDebounce.ts | 12 +
11 files changed, 1097 insertions(+), 114 deletions(-)
create mode 100644 src/renderer/features/tasks/components/BulkActionBar.tsx
create mode 100644 src/renderer/shared/hooks/useDebounce.ts
diff --git a/src/renderer/features/agent-dashboard/components/AgentDashboardPage.tsx b/src/renderer/features/agent-dashboard/components/AgentDashboardPage.tsx
index 97995229..c1dbf6b1 100644
--- a/src/renderer/features/agent-dashboard/components/AgentDashboardPage.tsx
+++ b/src/renderer/features/agent-dashboard/components/AgentDashboardPage.tsx
@@ -8,7 +8,7 @@
import { useCallback, useMemo, useState } from 'react';
-import { Bot } from 'lucide-react';
+import { Bot, Search } from 'lucide-react';
import type {
AgentDashboardFilters,
@@ -16,8 +16,11 @@ import type {
AgentLayoutMode,
AgentPanelState,
AgentSession,
+ AgentSessionType,
+ AgentStatus,
} from '@shared/types/agent-dashboard';
+import { useDebounce } from '@renderer/shared/hooks/useDebounce';
import { cn } from '@renderer/shared/lib/utils';
import {
@@ -31,6 +34,11 @@ import {
Label,
PageHeader,
PageLayout,
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
Separator,
Spinner,
Tabs,
@@ -52,6 +60,32 @@ import { RunningWorkflowsPanel } from './RunningWorkflowsPanel';
import { TemplateEditorPanel } from './TemplateEditorPanel';
import { TemplateListPanel } from './TemplateListPanel';
+// ─── Constants ─────────────────────────────────────────────
+
+const SESSION_TYPE_OPTIONS: Array<{ value: AgentSessionType | 'all'; label: string }> = [
+ { value: 'all', label: 'All types' },
+ { value: 'project-owner', label: 'Project owner' },
+ { value: 'team-lead', label: 'Team lead' },
+ { value: 'teammate', label: 'Teammate' },
+];
+
+const STATUS_OPTIONS: Array<{ value: AgentStatus | 'all'; label: string }> = [
+ { value: 'all', label: 'All statuses' },
+ { value: 'running', label: 'Running' },
+ { value: 'completed', label: 'Completed' },
+ { value: 'failed', label: 'Failed' },
+ { value: 'idle', label: 'Idle' },
+];
+
+// ─── Local filter state ─────────────────────────────────────
+
+interface SessionFilterState {
+ sessionType: AgentSessionType | 'all';
+ status: AgentStatus | 'all';
+ teamName: string;
+ search: string;
+}
+
// ─── Props ─────────────────────────────────────────────────
interface AgentDashboardPageProps {
@@ -72,6 +106,15 @@ export function AgentDashboardPage({
filters: {},
});
+ const [sessionFilters, setSessionFilters] = useState({
+ sessionType: 'all',
+ status: 'all',
+ teamName: 'all',
+ search: '',
+ });
+
+ const debouncedSearch = useDebounce(sessionFilters.search, 250);
+
const activeMainTab = useAgentDashboardStore((s) => s.activeMainTab);
const setActiveMainTab = useAgentDashboardStore((s) => s.setActiveMainTab);
@@ -118,58 +161,178 @@ export function AgentDashboardPage({
setAgentUiState((prev) => ({ ...prev, popupAgentId: agentId }));
}, []);
+ // ─── Team name options ────────────────────────────────
+
+ const teamNameOptions = useMemo(() => {
+ const names = Array.from(
+ new Set(agents.map((a) => a.teamName).filter((n): n is string => n !== undefined && n !== ''))
+ );
+ return names;
+ }, [agents]);
+
// ─── Filtered Agents ──────────────────────────────────
const filteredAgents = useMemo(() => {
let result = agents;
+
+ // Existing IPC-backed filters
if (agentUiState.filters.projectId !== undefined) {
result = result.filter((a) => a.projectId === agentUiState.filters.projectId);
}
if (agentUiState.filters.status !== undefined) {
result = result.filter((a) => a.status === agentUiState.filters.status);
}
+
+ // New client-side filters
+ if (sessionFilters.sessionType !== 'all') {
+ result = result.filter((a) => a.type === sessionFilters.sessionType);
+ }
+ if (sessionFilters.status !== 'all') {
+ result = result.filter((a) => a.status === sessionFilters.status);
+ }
+ if (sessionFilters.teamName !== 'all') {
+ result = result.filter((a) => a.teamName === sessionFilters.teamName);
+ }
+ if (debouncedSearch.trim().length > 0) {
+ const query = debouncedSearch.trim().toLowerCase();
+ result = result.filter((a) => a.name.toLowerCase().includes(query));
+ }
+
return result;
- }, [agents, agentUiState.filters]);
+ }, [agents, agentUiState.filters, sessionFilters, debouncedSearch]);
const popupAgent = useMemo(
() => agents.find((a) => a.id === agentUiState.popupAgentId),
[agents, agentUiState.popupAgentId],
);
+ // ─── Agents tab filter bar ────────────────────────────
+
+ function renderSessionFilterBar() {
+ return (
+
+ {/* Session type filter */}
+
+
+ {/* Status filter */}
+
+
+ {/* Team name filter */}
+
+
+ {/* Search by session name */}
+
+
+
+ setSessionFilters((prev) => ({ ...prev, search: e.target.value }))
+ }
+ />
+
+
+ );
+ }
+
// ─── Agents tab empty state ───────────────────────────
function renderAgentsContent() {
if (agents.length === 0) {
return (
-
-
- No agents running
-
- Start a session to see agent activity here
-
-
+ <>
+ {renderSessionFilterBar()}
+
+
+ No agents running
+
+ Start a session to see agent activity here
+
+
+ >
);
}
return (
<>
- {agentUiState.layoutMode === 'single' ? (
-
- ) : (
-
- )}
+ {renderSessionFilterBar()}
+
+ {agentUiState.layoutMode === 'single' ? (
+
+ ) : (
+
+ )}
+
{popupAgent === undefined ? null : (
-
+
{renderAgentsContent()}
diff --git a/src/renderer/features/my-work/components/MyWorkPage.tsx b/src/renderer/features/my-work/components/MyWorkPage.tsx
index 8d8b2347..dd4ca5e3 100644
--- a/src/renderer/features/my-work/components/MyWorkPage.tsx
+++ b/src/renderer/features/my-work/components/MyWorkPage.tsx
@@ -2,20 +2,26 @@
* MyWorkPage -- Cross-project task view
*
* Displays all progress tasks from SQLite, optionally grouped by team name.
- * Includes status filter for quick access to tasks by state.
+ * Includes status filter, search input, sort dropdown, priority/Jira/PR badges,
+ * and clickable rows that navigate to the project tasks view.
*/
import { useMemo, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
-import { Briefcase, Filter, RefreshCw, Users } from 'lucide-react';
+import { useNavigate } from '@tanstack/react-router';
+import { Briefcase, ExternalLink, Filter, RefreshCw, Users } from 'lucide-react';
+import { ROUTE_PATTERNS } from '@shared/constants';
import { PROGRESS_EVENTS } from '@shared/ipc/progress/channels';
-import type { ProgressStatus, ProgressTask } from '@shared/types/progress';
+import type { ProgressPriority, ProgressStatus, ProgressTask } from '@shared/types/progress';
import { useIpcEvent } from '@renderer/shared/hooks';
+import { useDebounce } from '@renderer/shared/hooks/useDebounce';
+import { useLayoutStore } from '@renderer/shared/stores/layout-store';
import {
+ Badge,
Button,
Card,
CardContent,
@@ -24,6 +30,7 @@ import {
PageContent,
PageHeader,
PageLayout,
+ SearchInput,
Select,
SelectContent,
SelectItem,
@@ -31,14 +38,20 @@ import {
SelectValue,
Separator,
StatusBadge,
+ Text,
} from '@ui';
-
import { myWorkKeys } from '../api/queryKeys';
import { useAllTasks } from '../api/useMyWork';
import type { StatusBadgeProps } from '@ui';
+/* ------------------------------------------------------------------ */
+/* Constants */
+/* ------------------------------------------------------------------ */
+
+const EXTERNAL_LINK_FEATURES = 'noopener,noreferrer';
+
/* ------------------------------------------------------------------ */
/* Status filter */
/* ------------------------------------------------------------------ */
@@ -59,6 +72,58 @@ const STATUS_OPTIONS: Array<{ value: StatusFilter; label: string }> = [
{ value: 'error', label: 'Error' },
];
+/* ------------------------------------------------------------------ */
+/* Sort options */
+/* ------------------------------------------------------------------ */
+
+type SortField = 'priority' | 'updatedAt' | 'status';
+
+const SORT_OPTIONS: Array<{ value: SortField; label: string }> = [
+ { value: 'priority', label: 'Priority' },
+ { value: 'updatedAt', label: 'Updated At' },
+ { value: 'status', label: 'Status' },
+];
+
+const PRIORITY_ORDER: Record = {
+ urgent: 0,
+ high: 1,
+ normal: 2,
+ low: 3,
+};
+
+const STATUS_ORDER: Record = {
+ error: 0,
+ executing: 1,
+ review: 2,
+ planning: 3,
+ plan_ready: 4,
+ researching: 5,
+ research_done: 6,
+ backlog: 7,
+ done: 8,
+ archived: 9,
+};
+
+/* ------------------------------------------------------------------ */
+/* Priority badge config */
+/* ------------------------------------------------------------------ */
+
+type BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline' | 'warning' | 'info' | 'success' | 'error';
+
+const PRIORITY_BADGE_VARIANTS: Record = {
+ low: 'outline',
+ normal: 'secondary',
+ high: 'info',
+ urgent: 'destructive',
+};
+
+const PRIORITY_LABELS: Record = {
+ low: 'Low',
+ normal: 'Normal',
+ high: 'High',
+ urgent: 'Urgent',
+};
+
/* ------------------------------------------------------------------ */
/* ProgressStatus badge config */
/* ------------------------------------------------------------------ */
@@ -101,7 +166,7 @@ function ProgressStatusBadge({
}
/* ------------------------------------------------------------------ */
-/* Grouping by team name */
+/* Filtering, sorting, grouping */
/* ------------------------------------------------------------------ */
interface TasksByTeam {
@@ -128,11 +193,33 @@ function groupTasksByTeam(tasks: ProgressTask[]): TasksByTeam[] {
return result;
}
-function filterTasks(tasks: ProgressTask[], status: StatusFilter): ProgressTask[] {
+function filterByStatus(tasks: ProgressTask[], status: StatusFilter): ProgressTask[] {
if (status === 'all') return tasks;
return tasks.filter((t) => t.status === status);
}
+function filterBySearch(tasks: ProgressTask[], query: string): ProgressTask[] {
+ if (query.trim().length === 0) return tasks;
+ const lower = query.toLowerCase();
+ return tasks.filter(
+ (t) =>
+ t.title.toLowerCase().includes(lower) ||
+ t.description.toLowerCase().includes(lower),
+ );
+}
+
+function sortTasks(tasks: ProgressTask[], field: SortField): ProgressTask[] {
+ const sorted = [...tasks];
+ if (field === 'priority') {
+ sorted.sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]);
+ } else if (field === 'updatedAt') {
+ sorted.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
+ } else {
+ sorted.sort((a, b) => STATUS_ORDER[a.status] - STATUS_ORDER[b.status]);
+ }
+ return sorted;
+}
+
function getTaskCountLabel(count: number): string {
return count === 1 ? 'task' : 'tasks';
}
@@ -144,7 +231,7 @@ function getTaskCountLabel(count: number): string {
function MyWorkEmptyState({ hasFilter }: { hasFilter: boolean }) {
const title = hasFilter ? 'No tasks match filter' : 'No tasks yet';
const description = hasFilter
- ? 'Try selecting a different status filter to see more tasks.'
+ ? 'Try selecting a different status filter or changing your search to see more tasks.'
: 'Tasks will appear here once you create them. Add a project and create tasks to get started.';
return (
@@ -157,7 +244,129 @@ function MyWorkEmptyState({ hasFilter }: { hasFilter: boolean }) {
);
}
-function TeamGroup({ group }: { group: TasksByTeam }) {
+interface TaskRowProps {
+ task: ProgressTask;
+ onNavigate: (task: ProgressTask) => void;
+}
+
+function TaskRow({ task, onNavigate }: TaskRowProps) {
+ const hasJira = task.jiraTicket !== undefined && task.jiraUrl !== undefined;
+ const hasPr = task.prNumber !== undefined && task.prUrl !== undefined;
+
+ function handleClick() {
+ onNavigate(task);
+ }
+
+ function handleKeyDown(e: React.KeyboardEvent) {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ onNavigate(task);
+ }
+ }
+
+ function handleJiraClick(e: React.MouseEvent) {
+ e.stopPropagation();
+ const url = task.jiraUrl;
+ if (url !== undefined) {
+ window.open(url, '_blank', EXTERNAL_LINK_FEATURES);
+ }
+ }
+
+ function handlePrClick(e: React.MouseEvent) {
+ e.stopPropagation();
+ const url = task.prUrl;
+ if (url !== undefined) {
+ window.open(url, '_blank', EXTERNAL_LINK_FEATURES);
+ }
+ }
+
+ function handleJiraKeyDown(e: React.KeyboardEvent) {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ e.stopPropagation();
+ const url = task.jiraUrl;
+ if (url !== undefined) {
+ window.open(url, '_blank', EXTERNAL_LINK_FEATURES);
+ }
+ }
+ }
+
+ function handlePrKeyDown(e: React.KeyboardEvent) {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ e.stopPropagation();
+ const url = task.prUrl;
+ if (url !== undefined) {
+ window.open(url, '_blank', EXTERNAL_LINK_FEATURES);
+ }
+ }
+ }
+
+ return (
+
+
+
+
+ {task.title}
+
+
+ {PRIORITY_LABELS[task.priority]}
+
+ {hasJira ? (
+
+
+ {task.jiraTicket}
+
+
+
+ ) : null}
+ {hasPr ? (
+
+
+ PR #{task.prNumber}
+
+
+
+ ) : null}
+
+
+
+ {task.description.length > 0 ? (
+
+ {task.description}
+
+ ) : null}
+
+ );
+}
+
+function TeamGroup({
+ group,
+ onNavigate,
+}: {
+ group: TasksByTeam;
+ onNavigate: (task: ProgressTask) => void;
+}) {
return (
@@ -169,18 +378,11 @@ function TeamGroup({ group }: { group: TasksByTeam }) {
{group.tasks.map((task) => (
-
-
- {task.description ? (
-
{task.description}
- ) : null}
-
+ task={task}
+ onNavigate={onNavigate}
+ />
))}
@@ -211,12 +413,14 @@ function TaskListContent({
taskGroups,
hasFilter,
onRetry,
+ onNavigate,
}: {
isLoading: boolean;
isError: boolean;
taskGroups: TasksByTeam[];
hasFilter: boolean;
onRetry: () => void;
+ onNavigate: (task: ProgressTask) => void;
}) {
if (isError) {
return ;
@@ -240,6 +444,7 @@ function TaskListContent({
))}
@@ -252,7 +457,15 @@ function TaskListContent({
export function MyWorkPage() {
const queryClient = useQueryClient();
+ const navigate = useNavigate();
+ const activeProjectId = useLayoutStore((s) => s.activeProjectId);
+
const [statusFilter, setStatusFilter] = useState('all');
+ const [searchQuery, setSearchQuery] = useState('');
+ const [sortField, setSortField] = useState('priority');
+
+ const debouncedSearch = useDebounce(searchQuery, 250);
+
const { data: tasks, isLoading: tasksLoading, isError: tasksError } = useAllTasks();
// Invalidate task list on progress events
@@ -270,17 +483,28 @@ export function MyWorkPage() {
void queryClient.invalidateQueries({ queryKey: myWorkKeys.tasks() });
}
- // Filter and group tasks
- const filteredTasks = useMemo(() => {
- return filterTasks(tasks ?? [], statusFilter);
- }, [tasks, statusFilter]);
+ function handleTaskNavigate(task: ProgressTask) {
+ if (!activeProjectId) return;
+ void navigate({
+ to: ROUTE_PATTERNS.PROJECT_TASKS,
+ params: { projectId: activeProjectId },
+ search: { taskSlug: task.slug },
+ });
+ }
+
+ // Filter and sort tasks
+ const processedTasks = useMemo(() => {
+ const statusFiltered = filterByStatus(tasks ?? [], statusFilter);
+ const searchFiltered = filterBySearch(statusFiltered, debouncedSearch);
+ return sortTasks(searchFiltered, sortField);
+ }, [tasks, statusFilter, debouncedSearch, sortField]);
const taskGroups = useMemo(() => {
- return groupTasksByTeam(filteredTasks);
- }, [filteredTasks]);
+ return groupTasksByTeam(processedTasks);
+ }, [processedTasks]);
- const totalTasks = filteredTasks.length;
- const hasFilter = statusFilter !== 'all';
+ const totalTasks = processedTasks.length;
+ const hasFilter = statusFilter !== 'all' || debouncedSearch.trim().length > 0;
return (
@@ -290,8 +514,15 @@ export function MyWorkPage() {
My Work
+ { setSearchQuery(e.target.value); }}
+ onClear={() => { setSearchQuery(''); }}
+ />
-
@@ -315,6 +558,7 @@ export function MyWorkPage() {
isError={tasksError}
isLoading={tasksLoading}
taskGroups={taskGroups}
+ onNavigate={handleTaskNavigate}
onRetry={handleRetry}
/>
diff --git a/src/renderer/features/personal/alerts/components/AlertsPage.tsx b/src/renderer/features/personal/alerts/components/AlertsPage.tsx
index aee87b44..11580373 100644
--- a/src/renderer/features/personal/alerts/components/AlertsPage.tsx
+++ b/src/renderer/features/personal/alerts/components/AlertsPage.tsx
@@ -9,9 +9,24 @@ import { Bell, Check, Clock, Pencil, Plus, Repeat, Trash2 } from 'lucide-react';
import type { Alert } from '@shared/types';
import { RelativeTime } from '@renderer/shared/components/RelativeTime';
+import { useDebounce } from '@renderer/shared/hooks/useDebounce';
import { cn } from '@renderer/shared/lib/utils';
-import { Badge, Button, EmptyState, PageContent, PageHeader, PageLayout } from '@ui';
+import {
+ Badge,
+ Button,
+ EmptyState,
+ PageContent,
+ PageHeader,
+ PageLayout,
+ SearchInput,
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+ Text,
+} from '@ui';
import { useAlerts, useDeleteAlert, useDismissAlert } from '../api/useAlerts';
import { useAlertEvents } from '../hooks/useAlertEvents';
@@ -22,6 +37,8 @@ import { CreateAlertModal } from './CreateAlertModal';
import { RecurringAlerts } from './RecurringAlerts';
type TabId = 'active' | 'dismissed' | 'recurring';
+type SortField = 'triggerAt' | 'createdAt';
+type AlertTypeFilter = 'all' | Alert['type'];
function getAlertIcon(type: Alert['type']) {
switch (type) {
@@ -56,6 +73,30 @@ function formatTriggerTime(triggerAt: string): string {
});
}
+function applyFilters(
+ alertList: Alert[],
+ searchQuery: string,
+ typeFilter: AlertTypeFilter,
+ sortField: SortField,
+): Alert[] {
+ let result = alertList;
+
+ if (searchQuery.length > 0) {
+ const lower = searchQuery.toLowerCase();
+ result = result.filter((a) => a.message.toLowerCase().includes(lower));
+ }
+
+ if (typeFilter !== 'all') {
+ result = result.filter((a) => a.type === typeFilter);
+ }
+
+ return [...result].sort((a, b) => {
+ const aVal = new Date(a[sortField]).getTime();
+ const bVal = new Date(b[sortField]).getTime();
+ return aVal - bVal;
+ });
+}
+
export function AlertsPage() {
useAlertEvents();
@@ -65,10 +106,18 @@ export function AlertsPage() {
const openCreateModal = useAlertStore((s) => s.openCreateModal);
const [editingAlert, setEditingAlert] = useState(null);
+ const [searchText, setSearchText] = useState('');
+ const [sortField, setSortField] = useState('triggerAt');
+ const [typeFilter, setTypeFilter] = useState('all');
+
+ const debouncedSearch = useDebounce(searchText, 250);
const activeAlerts = alerts.filter((a) => !a.dismissed);
const dismissedAlerts = alerts.filter((a) => a.dismissed);
+ const filteredActive = applyFilters(activeAlerts, debouncedSearch, typeFilter, sortField);
+ const filteredDismissed = applyFilters(dismissedAlerts, debouncedSearch, typeFilter, sortField);
+
const tabs: Array<{ id: TabId; label: string; count: number }> = [
{ id: 'active', label: 'Active', count: activeAlerts.length },
{ id: 'dismissed', label: 'Dismissed', count: dismissedAlerts.length },
@@ -113,8 +162,8 @@ export function AlertsPage() {
/>
-
{alert.message}
-
{alert.message}
+
{formatTriggerTime(alert.triggerAt)}
{alert.recurring === undefined ? '' : ' (recurring)'}
-
+
{alert.linkedTo === undefined ? null : (
@@ -197,6 +246,48 @@ export function AlertsPage() {
))}
+
+ {/* Filter toolbar */}
+
+ setSearchText(e.target.value)}
+ onClear={() => setSearchText('')}
+ />
+
+ setTypeFilter(v as AlertTypeFilter)}
+ >
+
+
+
+
+ All types
+ Reminder
+ Deadline
+ Notification
+ Recurring
+
+
+
+ setSortField(v as SortField)}
+ >
+
+
+
+
+ Trigger time
+ Created time
+
+
+
+
{isLoading ? (
@@ -204,8 +295,8 @@ export function AlertsPage() {
) : (
<>
- {renderAlertList(activeAlerts)}
- {renderAlertList(dismissedAlerts)}
+ {renderAlertList(filteredActive)}
+ {renderAlertList(filteredDismissed)}
diff --git a/src/renderer/features/personal/fitness/components/BodyComposition.tsx b/src/renderer/features/personal/fitness/components/BodyComposition.tsx
index fc084f21..f0b98911 100644
--- a/src/renderer/features/personal/fitness/components/BodyComposition.tsx
+++ b/src/renderer/features/personal/fitness/components/BodyComposition.tsx
@@ -23,6 +23,7 @@ import {
Card,
CardContent,
EmptyState,
+ Heading,
Input,
Label,
Text,
@@ -40,8 +41,17 @@ export function BodyComposition() {
const [showForm, setShowForm] = useState(false);
const [weight, setWeight] = useState('');
const [bodyFat, setBodyFat] = useState('');
+ const [dateFrom, setDateFrom] = useState('');
+ const [dateTo, setDateTo] = useState('');
+
+ const allMeasurements = measurements ?? [];
+
+ const displayMeasurements = allMeasurements.filter((m) => {
+ if (dateFrom && m.date < dateFrom) return false;
+ if (dateTo && m.date > dateTo) return false;
+ return true;
+ });
- const displayMeasurements = measurements ?? [];
const latest = displayMeasurements.length > 0 ? displayMeasurements[0] : null;
function handleSubmit() {
@@ -67,15 +77,53 @@ export function BodyComposition() {
);
}
+ const isFiltered = dateFrom !== '' || dateTo !== '';
+
return (
+ {/* Date-range filter */}
+
+
+
+ setDateFrom(e.target.value)}
+ />
+
+
+
+ setDateTo(e.target.value)}
+ />
+
+ {isFiltered ? (
+
{ setDateFrom(''); setDateTo(''); }}
+ >
+ Clear
+
+ ) : null}
+
+
{/* Latest measurements */}
{latest ? (
-
+
Latest Measurements
-
+
{latest.weight === undefined ? null : (
@@ -123,7 +171,7 @@ export function BodyComposition() {
) : (
- Log Measurement
+ Log Measurement