mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 21:23:07 +00:00
feat: Add validation viewing functionality and UI updates
- Implemented a new function to mark validations as viewed by the user, updating the validation state accordingly. - Added a new API endpoint for marking validations as viewed, integrated with the existing GitHub routes. - Enhanced the sidebar to display the count of unviewed validations, providing real-time updates. - Updated the GitHub issues view to mark validations as viewed when issues are accessed, improving user interaction. - Introduced a visual indicator for unviewed validations in the issue list, enhancing user awareness of pending validations.
This commit is contained in:
@@ -145,3 +145,35 @@ export async function getValidationWithFreshness(
|
|||||||
isStale: isValidationStale(validation),
|
isStale: isValidationStale(validation),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a validation as viewed by the user
|
||||||
|
*
|
||||||
|
* @param projectPath - Absolute path to project directory
|
||||||
|
* @param issueNumber - GitHub issue number
|
||||||
|
* @returns true if validation was marked as viewed, false if not found
|
||||||
|
*/
|
||||||
|
export async function markValidationViewed(
|
||||||
|
projectPath: string,
|
||||||
|
issueNumber: number
|
||||||
|
): Promise<boolean> {
|
||||||
|
const validation = await readValidation(projectPath, issueNumber);
|
||||||
|
if (!validation) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
validation.viewedAt = new Date().toISOString();
|
||||||
|
await writeValidation(projectPath, issueNumber, validation);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get count of unviewed, non-stale validations for a project
|
||||||
|
*
|
||||||
|
* @param projectPath - Absolute path to project directory
|
||||||
|
* @returns Number of unviewed validations
|
||||||
|
*/
|
||||||
|
export async function getUnviewedValidationsCount(projectPath: string): Promise<number> {
|
||||||
|
const validations = await getAllValidations(projectPath);
|
||||||
|
return validations.filter((v) => !v.viewedAt && !isValidationStale(v)).length;
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
createValidationStopHandler,
|
createValidationStopHandler,
|
||||||
createGetValidationsHandler,
|
createGetValidationsHandler,
|
||||||
createDeleteValidationHandler,
|
createDeleteValidationHandler,
|
||||||
|
createMarkViewedHandler,
|
||||||
} from './routes/validation-endpoints.js';
|
} from './routes/validation-endpoints.js';
|
||||||
|
|
||||||
export function createGitHubRoutes(events: EventEmitter): Router {
|
export function createGitHubRoutes(events: EventEmitter): Router {
|
||||||
@@ -41,6 +42,11 @@ export function createGitHubRoutes(events: EventEmitter): Router {
|
|||||||
validatePathParams('projectPath'),
|
validatePathParams('projectPath'),
|
||||||
createDeleteValidationHandler()
|
createDeleteValidationHandler()
|
||||||
);
|
);
|
||||||
|
router.post(
|
||||||
|
'/validation-mark-viewed',
|
||||||
|
validatePathParams('projectPath'),
|
||||||
|
createMarkViewedHandler()
|
||||||
|
);
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
getAllValidations,
|
getAllValidations,
|
||||||
getValidationWithFreshness,
|
getValidationWithFreshness,
|
||||||
deleteValidation,
|
deleteValidation,
|
||||||
|
markValidationViewed,
|
||||||
} from '../../../lib/validation-storage.js';
|
} from '../../../lib/validation-storage.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -188,3 +189,36 @@ export function createDeleteValidationHandler() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /validation-mark-viewed - Mark a validation as viewed by the user
|
||||||
|
*/
|
||||||
|
export function createMarkViewedHandler() {
|
||||||
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { projectPath, issueNumber } = req.body as {
|
||||||
|
projectPath: string;
|
||||||
|
issueNumber: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!projectPath) {
|
||||||
|
res.status(400).json({ success: false, error: 'projectPath is required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!issueNumber || typeof issueNumber !== 'number') {
|
||||||
|
res
|
||||||
|
.status(400)
|
||||||
|
.json({ success: false, error: 'issueNumber is required and must be a number' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = await markValidationViewed(projectPath, issueNumber);
|
||||||
|
|
||||||
|
res.json({ success });
|
||||||
|
} catch (error) {
|
||||||
|
logError(error, 'Mark validation viewed failed');
|
||||||
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
useSetupDialog,
|
useSetupDialog,
|
||||||
useTrashDialog,
|
useTrashDialog,
|
||||||
useProjectTheme,
|
useProjectTheme,
|
||||||
|
useUnviewedValidations,
|
||||||
} from './sidebar/hooks';
|
} from './sidebar/hooks';
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
@@ -127,6 +128,9 @@ export function Sidebar() {
|
|||||||
// Running agents count
|
// Running agents count
|
||||||
const { runningAgentsCount } = useRunningAgents();
|
const { runningAgentsCount } = useRunningAgents();
|
||||||
|
|
||||||
|
// Unviewed validations count
|
||||||
|
const { count: unviewedValidationsCount } = useUnviewedValidations(currentProject);
|
||||||
|
|
||||||
// Trash dialog and operations
|
// Trash dialog and operations
|
||||||
const {
|
const {
|
||||||
showTrashDialog,
|
showTrashDialog,
|
||||||
@@ -235,6 +239,7 @@ export function Sidebar() {
|
|||||||
setIsProjectPickerOpen,
|
setIsProjectPickerOpen,
|
||||||
cyclePrevProject,
|
cyclePrevProject,
|
||||||
cycleNextProject,
|
cycleNextProject,
|
||||||
|
unviewedValidationsCount,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register keyboard shortcuts
|
// Register keyboard shortcuts
|
||||||
|
|||||||
@@ -78,14 +78,29 @@ export function SidebarNavigation({
|
|||||||
title={!sidebarOpen ? item.label : undefined}
|
title={!sidebarOpen ? item.label : undefined}
|
||||||
data-testid={`nav-${item.id}`}
|
data-testid={`nav-${item.id}`}
|
||||||
>
|
>
|
||||||
<Icon
|
<div className="relative">
|
||||||
className={cn(
|
<Icon
|
||||||
'w-[18px] h-[18px] shrink-0 transition-all duration-200',
|
className={cn(
|
||||||
isActive
|
'w-[18px] h-[18px] shrink-0 transition-all duration-200',
|
||||||
? 'text-brand-500 drop-shadow-sm'
|
isActive
|
||||||
: 'group-hover:text-brand-400 group-hover:scale-110'
|
? 'text-brand-500 drop-shadow-sm'
|
||||||
|
: 'group-hover:text-brand-400 group-hover:scale-110'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{/* Count badge for collapsed state */}
|
||||||
|
{!sidebarOpen && item.count !== undefined && item.count > 0 && (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'absolute -top-1.5 -right-1.5 flex items-center justify-center',
|
||||||
|
'min-w-4 h-4 px-1 text-[9px] font-bold rounded-full',
|
||||||
|
'bg-primary text-primary-foreground shadow-sm',
|
||||||
|
'animate-in fade-in zoom-in duration-200'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.count > 99 ? '99' : item.count}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
/>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'ml-3 font-medium text-sm flex-1 text-left',
|
'ml-3 font-medium text-sm flex-1 text-left',
|
||||||
@@ -94,7 +109,21 @@ export function SidebarNavigation({
|
|||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
{item.shortcut && sidebarOpen && (
|
{/* Count badge */}
|
||||||
|
{item.count !== undefined && item.count > 0 && sidebarOpen && (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'hidden lg:flex items-center justify-center',
|
||||||
|
'min-w-5 h-5 px-1.5 text-[10px] font-bold rounded-full',
|
||||||
|
'bg-primary text-primary-foreground shadow-sm',
|
||||||
|
'animate-in fade-in zoom-in duration-200'
|
||||||
|
)}
|
||||||
|
data-testid={`count-${item.id}`}
|
||||||
|
>
|
||||||
|
{item.count > 99 ? '99+' : item.count}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{item.shortcut && sidebarOpen && !item.count && (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'hidden lg:flex items-center justify-center min-w-5 h-5 px-1.5 text-[10px] font-mono rounded-md transition-all duration-200',
|
'hidden lg:flex items-center justify-center min-w-5 h-5 px-1.5 text-[10px] font-mono rounded-md transition-all duration-200',
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ export { useProjectCreation } from './use-project-creation';
|
|||||||
export { useSetupDialog } from './use-setup-dialog';
|
export { useSetupDialog } from './use-setup-dialog';
|
||||||
export { useTrashDialog } from './use-trash-dialog';
|
export { useTrashDialog } from './use-trash-dialog';
|
||||||
export { useProjectTheme } from './use-project-theme';
|
export { useProjectTheme } from './use-project-theme';
|
||||||
|
export { useUnviewedValidations } from './use-unviewed-validations';
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ interface UseNavigationProps {
|
|||||||
setIsProjectPickerOpen: (value: boolean | ((prev: boolean) => boolean)) => void;
|
setIsProjectPickerOpen: (value: boolean | ((prev: boolean) => boolean)) => void;
|
||||||
cyclePrevProject: () => void;
|
cyclePrevProject: () => void;
|
||||||
cycleNextProject: () => void;
|
cycleNextProject: () => void;
|
||||||
|
/** Count of unviewed validations to show on GitHub Issues nav item */
|
||||||
|
unviewedValidationsCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useNavigation({
|
export function useNavigation({
|
||||||
@@ -61,6 +63,7 @@ export function useNavigation({
|
|||||||
setIsProjectPickerOpen,
|
setIsProjectPickerOpen,
|
||||||
cyclePrevProject,
|
cyclePrevProject,
|
||||||
cycleNextProject,
|
cycleNextProject,
|
||||||
|
unviewedValidationsCount,
|
||||||
}: UseNavigationProps) {
|
}: UseNavigationProps) {
|
||||||
// Track if current project has a GitHub remote
|
// Track if current project has a GitHub remote
|
||||||
const [hasGitHubRemote, setHasGitHubRemote] = useState(false);
|
const [hasGitHubRemote, setHasGitHubRemote] = useState(false);
|
||||||
@@ -169,6 +172,7 @@ export function useNavigation({
|
|||||||
id: 'github-issues',
|
id: 'github-issues',
|
||||||
label: 'Issues',
|
label: 'Issues',
|
||||||
icon: CircleDot,
|
icon: CircleDot,
|
||||||
|
count: unviewedValidationsCount,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'github-prs',
|
id: 'github-prs',
|
||||||
@@ -180,7 +184,15 @@ export function useNavigation({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return sections;
|
return sections;
|
||||||
}, [shortcuts, hideSpecEditor, hideContext, hideTerminal, hideAiProfiles, hasGitHubRemote]);
|
}, [
|
||||||
|
shortcuts,
|
||||||
|
hideSpecEditor,
|
||||||
|
hideContext,
|
||||||
|
hideTerminal,
|
||||||
|
hideAiProfiles,
|
||||||
|
hasGitHubRemote,
|
||||||
|
unviewedValidationsCount,
|
||||||
|
]);
|
||||||
|
|
||||||
// Build keyboard shortcuts for navigation
|
// Build keyboard shortcuts for navigation
|
||||||
const navigationShortcuts: KeyboardShortcut[] = useMemo(() => {
|
const navigationShortcuts: KeyboardShortcut[] = useMemo(() => {
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
|
import type { Project, StoredValidation } from '@/lib/electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to track the count of unviewed (fresh) issue validations for a project.
|
||||||
|
* Also provides a function to decrement the count when a validation is viewed.
|
||||||
|
*/
|
||||||
|
export function useUnviewedValidations(currentProject: Project | null) {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
|
||||||
|
// Load initial count
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentProject?.path) {
|
||||||
|
setCount(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadCount = async () => {
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (api.github?.getValidations) {
|
||||||
|
const result = await api.github.getValidations(currentProject.path);
|
||||||
|
if (result.success && result.validations) {
|
||||||
|
const unviewed = result.validations.filter((v: StoredValidation) => {
|
||||||
|
if (v.viewedAt) return false;
|
||||||
|
// Check if not stale (< 24 hours)
|
||||||
|
const hoursSince =
|
||||||
|
(Date.now() - new Date(v.validatedAt).getTime()) / (1000 * 60 * 60);
|
||||||
|
return hoursSince <= 24;
|
||||||
|
});
|
||||||
|
setCount(unviewed.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[useUnviewedValidations] Failed to load count:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadCount();
|
||||||
|
|
||||||
|
// Subscribe to validation events to update count
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (api.github?.onValidationEvent) {
|
||||||
|
const unsubscribe = api.github.onValidationEvent((event) => {
|
||||||
|
if (event.projectPath === currentProject.path) {
|
||||||
|
if (event.type === 'issue_validation_complete') {
|
||||||
|
setCount((prev) => prev + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => unsubscribe();
|
||||||
|
}
|
||||||
|
}, [currentProject?.path]);
|
||||||
|
|
||||||
|
// Function to decrement count when a validation is viewed
|
||||||
|
const decrementCount = useCallback(() => {
|
||||||
|
setCount((prev) => Math.max(0, prev - 1));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Function to refresh count (e.g., after marking as viewed)
|
||||||
|
const refreshCount = useCallback(async () => {
|
||||||
|
if (!currentProject?.path) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (api.github?.getValidations) {
|
||||||
|
const result = await api.github.getValidations(currentProject.path);
|
||||||
|
if (result.success && result.validations) {
|
||||||
|
const unviewed = result.validations.filter((v: StoredValidation) => {
|
||||||
|
if (v.viewedAt) return false;
|
||||||
|
const hoursSince = (Date.now() - new Date(v.validatedAt).getTime()) / (1000 * 60 * 60);
|
||||||
|
return hoursSince <= 24;
|
||||||
|
});
|
||||||
|
setCount(unviewed.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[useUnviewedValidations] Failed to refresh count:', err);
|
||||||
|
}
|
||||||
|
}, [currentProject?.path]);
|
||||||
|
|
||||||
|
return { count, decrementCount, refreshCount };
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ export interface NavItem {
|
|||||||
label: string;
|
label: string;
|
||||||
icon: React.ComponentType<{ className?: string }>;
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
shortcut?: string;
|
shortcut?: string;
|
||||||
|
/** Optional count badge to display next to the nav item */
|
||||||
|
count?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SortableProjectItemProps {
|
export interface SortableProjectItemProps {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
User,
|
User,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
Clock,
|
Clock,
|
||||||
|
Sparkles,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
getElectronAPI,
|
getElectronAPI,
|
||||||
@@ -119,6 +120,27 @@ export function GitHubIssuesView() {
|
|||||||
loadCachedValidations();
|
loadCachedValidations();
|
||||||
}, [currentProject?.path]);
|
}, [currentProject?.path]);
|
||||||
|
|
||||||
|
// Load running validations on mount (restore validatingIssues state)
|
||||||
|
useEffect(() => {
|
||||||
|
const loadRunningValidations = async () => {
|
||||||
|
if (!currentProject?.path) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (api.github?.getValidationStatus) {
|
||||||
|
const result = await api.github.getValidationStatus(currentProject.path);
|
||||||
|
if (result.success && result.runningIssues) {
|
||||||
|
setValidatingIssues(new Set(result.runningIssues));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[GitHubIssuesView] Failed to load running validations:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadRunningValidations();
|
||||||
|
}, [currentProject?.path]);
|
||||||
|
|
||||||
// Subscribe to validation events
|
// Subscribe to validation events
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
@@ -293,14 +315,38 @@ export function GitHubIssuesView() {
|
|||||||
|
|
||||||
// View cached validation result
|
// View cached validation result
|
||||||
const handleViewCachedValidation = useCallback(
|
const handleViewCachedValidation = useCallback(
|
||||||
(issue: GitHubIssue) => {
|
async (issue: GitHubIssue) => {
|
||||||
const cached = cachedValidations.get(issue.number);
|
const cached = cachedValidations.get(issue.number);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
setValidationResult(cached.result);
|
setValidationResult(cached.result);
|
||||||
setShowValidationDialog(true);
|
setShowValidationDialog(true);
|
||||||
|
|
||||||
|
// Mark as viewed if not already viewed
|
||||||
|
if (!cached.viewedAt && currentProject?.path) {
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (api.github?.markValidationViewed) {
|
||||||
|
await api.github.markValidationViewed(currentProject.path, issue.number);
|
||||||
|
// Update local state
|
||||||
|
setCachedValidations((prev) => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
const updated = prev.get(issue.number);
|
||||||
|
if (updated) {
|
||||||
|
next.set(issue.number, {
|
||||||
|
...updated,
|
||||||
|
viewedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[GitHubIssuesView] Failed to mark validation as viewed:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[cachedValidations]
|
[cachedValidations, currentProject?.path]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleConvertToTask = useCallback(
|
const handleConvertToTask = useCallback(
|
||||||
@@ -446,6 +492,7 @@ export function GitHubIssuesView() {
|
|||||||
onClick={() => setSelectedIssue(issue)}
|
onClick={() => setSelectedIssue(issue)}
|
||||||
onOpenExternal={() => handleOpenInGitHub(issue.url)}
|
onOpenExternal={() => handleOpenInGitHub(issue.url)}
|
||||||
formatDate={formatDate}
|
formatDate={formatDate}
|
||||||
|
cachedValidation={cachedValidations.get(issue.number)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -463,6 +510,7 @@ export function GitHubIssuesView() {
|
|||||||
onClick={() => setSelectedIssue(issue)}
|
onClick={() => setSelectedIssue(issue)}
|
||||||
onOpenExternal={() => handleOpenInGitHub(issue.url)}
|
onOpenExternal={() => handleOpenInGitHub(issue.url)}
|
||||||
formatDate={formatDate}
|
formatDate={formatDate}
|
||||||
|
cachedValidation={cachedValidations.get(issue.number)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
@@ -726,9 +774,27 @@ interface IssueRowProps {
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
onOpenExternal: () => void;
|
onOpenExternal: () => void;
|
||||||
formatDate: (date: string) => string;
|
formatDate: (date: string) => string;
|
||||||
|
/** Cached validation for this issue (if any) */
|
||||||
|
cachedValidation?: StoredValidation | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function IssueRow({ issue, isSelected, onClick, onOpenExternal, formatDate }: IssueRowProps) {
|
function IssueRow({
|
||||||
|
issue,
|
||||||
|
isSelected,
|
||||||
|
onClick,
|
||||||
|
onOpenExternal,
|
||||||
|
formatDate,
|
||||||
|
cachedValidation,
|
||||||
|
}: IssueRowProps) {
|
||||||
|
// Check if validation is unviewed (exists, not stale, not viewed)
|
||||||
|
const hasUnviewedValidation =
|
||||||
|
cachedValidation &&
|
||||||
|
!cachedValidation.viewedAt &&
|
||||||
|
(() => {
|
||||||
|
const hoursSince =
|
||||||
|
(Date.now() - new Date(cachedValidation.validatedAt).getTime()) / (1000 * 60 * 60);
|
||||||
|
return hoursSince <= 24;
|
||||||
|
})();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -785,6 +851,14 @@ function IssueRow({ issue, isSelected, onClick, onOpenExternal, formatDate }: Is
|
|||||||
{issue.assignees.map((a) => a.login).join(', ')}
|
{issue.assignees.map((a) => a.login).join(', ')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Unviewed validation indicator */}
|
||||||
|
{hasUnviewedValidation && (
|
||||||
|
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded-full bg-amber-500/10 text-amber-500 border border-amber-500/20 animate-in fade-in duration-200">
|
||||||
|
<Sparkles className="h-3 w-3" />
|
||||||
|
Analysis Ready
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -227,6 +227,11 @@ export interface GitHubAPI {
|
|||||||
isStale?: boolean;
|
isStale?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
}>;
|
}>;
|
||||||
|
/** Mark a validation as viewed by the user */
|
||||||
|
markValidationViewed: (
|
||||||
|
projectPath: string,
|
||||||
|
issueNumber: number
|
||||||
|
) => Promise<{ success: boolean; error?: string }>;
|
||||||
/** Subscribe to validation events */
|
/** Subscribe to validation events */
|
||||||
onValidationEvent: (callback: (event: IssueValidationEvent) => void) => () => void;
|
onValidationEvent: (callback: (event: IssueValidationEvent) => void) => () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -762,6 +762,8 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
this.post('/api/github/validation-stop', { projectPath, issueNumber }),
|
this.post('/api/github/validation-stop', { projectPath, issueNumber }),
|
||||||
getValidations: (projectPath: string, issueNumber?: number) =>
|
getValidations: (projectPath: string, issueNumber?: number) =>
|
||||||
this.post('/api/github/validations', { projectPath, issueNumber }),
|
this.post('/api/github/validations', { projectPath, issueNumber }),
|
||||||
|
markValidationViewed: (projectPath: string, issueNumber: number) =>
|
||||||
|
this.post('/api/github/validation-mark-viewed', { projectPath, issueNumber }),
|
||||||
onValidationEvent: (callback: (event: IssueValidationEvent) => void) =>
|
onValidationEvent: (callback: (event: IssueValidationEvent) => void) =>
|
||||||
this.subscribeToEvent('issue-validation:event', callback as EventCallback),
|
this.subscribeToEvent('issue-validation:event', callback as EventCallback),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -123,4 +123,6 @@ export interface StoredValidation {
|
|||||||
model: AgentModel;
|
model: AgentModel;
|
||||||
/** The validation result */
|
/** The validation result */
|
||||||
result: IssueValidationResult;
|
result: IssueValidationResult;
|
||||||
|
/** ISO timestamp when user viewed this validation (undefined = not yet viewed) */
|
||||||
|
viewedAt?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user