Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions archon-ui-main/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions archon-ui-main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"fractional-indexing": "^3.2.0",
"framer-motion": "^11.5.4",
"lucide-react": "^0.441.0",
"nanoid": "^5.0.9",
"prismjs": "^1.30.0",
"react": "^18.3.1",
"react-dnd": "^16.0.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import { format } from "date-fns";
import { motion } from "framer-motion";
import { Briefcase, Clock, Code, ExternalLink, File, FileText, Globe, Terminal } from "lucide-react";
import { useState } from "react";
import { isOptimistic } from "../../shared/optimistic";
import { KnowledgeCardProgress } from "../../progress/components/KnowledgeCardProgress";
import type { ActiveOperation } from "../../progress/types";
import { StatPill } from "../../ui/primitives";
import { OptimisticIndicator } from "../../ui/primitives/OptimisticIndicator";
import { cn } from "../../ui/primitives/styles";
import { SimpleTooltip } from "../../ui/primitives/tooltip";
import { useDeleteKnowledgeItem, useRefreshKnowledgeItem } from "../hooks";
Expand Down Expand Up @@ -44,6 +46,9 @@ export const KnowledgeCard: React.FC<KnowledgeCardProps> = ({
const deleteMutation = useDeleteKnowledgeItem();
const refreshMutation = useRefreshKnowledgeItem();

// Check if item is optimistic
const optimistic = isOptimistic(item);

// Determine card styling based on type and status
// Check if it's a real URL (not a file:// URL)
// Prioritize top-level source_type over metadata source_type
Expand Down Expand Up @@ -168,6 +173,7 @@ export const KnowledgeCard: React.FC<KnowledgeCardProps> = ({
getBorderColor(),
isHovered && "shadow-[0_0_30px_rgba(6,182,212,0.2)]",
"min-h-[240px] flex flex-col",
optimistic && "opacity-80 ring-1 ring-cyan-400/30",
)}
>
{/* Top accent glow tied to type (does not change size) */}
Expand Down Expand Up @@ -235,6 +241,7 @@ export const KnowledgeCard: React.FC<KnowledgeCardProps> = ({
description={item.metadata?.description}
accentColor={getAccentColorName()}
/>
<OptimisticIndicator isOptimistic={optimistic} className="mt-2" />
</div>

{/* URL/Source */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect, useMemo, useState } from "react";
import { createOptimisticId } from "@/features/shared/optimistic";
import { useActiveOperations } from "../../progress/hooks";
import { progressKeys } from "../../progress/hooks/useProgressQueries";
import type { ActiveOperation, ActiveOperationsResponse } from "../../progress/types";
Expand Down Expand Up @@ -139,8 +140,8 @@ export function useCrawlUrl() {
const previousOperations = queryClient.getQueryData<ActiveOperationsResponse>(progressKeys.active());

// Generate temporary IDs
const tempProgressId = `temp-progress-${Date.now()}`;
const tempItemId = `temp-item-${Date.now()}`;
const tempProgressId = createOptimisticId();
const tempItemId = createOptimisticId();

// Create optimistic knowledge item
const optimisticItem: KnowledgeItem = {
Expand Down Expand Up @@ -353,8 +354,8 @@ export function useUploadDocument() {
const previousOperations = queryClient.getQueryData<ActiveOperationsResponse>(progressKeys.active());

// Generate temporary IDs
const tempProgressId = `temp-upload-${Date.now()}`;
const tempItemId = `temp-item-${Date.now()}`;
const tempProgressId = createOptimisticId();
const tempItemId = createOptimisticId();

// Create optimistic knowledge item for the upload
const optimisticItem: KnowledgeItem = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { motion } from "framer-motion";
import { Activity, CheckCircle2, ListTodo } from "lucide-react";
import type React from "react";
import { isOptimistic } from "../../shared/optimistic";
import { OptimisticIndicator } from "../../ui/primitives/OptimisticIndicator";
import { cn } from "../../ui/primitives/styles";
import type { Project } from "../types";
import { ProjectCardActions } from "./ProjectCardActions";
Expand All @@ -27,6 +29,9 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
onPin,
onDelete,
}) => {
// Check if project is optimistic
const optimistic = isOptimistic(project);

return (
<motion.div
tabIndex={0}
Expand Down Expand Up @@ -59,6 +64,7 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
: "shadow-[0_10px_30px_-15px_rgba(0,0,0,0.1)] dark:shadow-[0_10px_30px_-15px_rgba(0,0,0,0.7)]",
"hover:shadow-[0_15px_40px_-15px_rgba(0,0,0,0.2)] dark:hover:shadow-[0_15px_40px_-15px_rgba(0,0,0,0.9)]",
isSelected ? "scale-[1.02]" : "hover:scale-[1.01]", // Use scale instead of translate to avoid clipping
optimistic && "opacity-80 ring-1 ring-cyan-400/30",
)}
>
{/* Subtle aurora glow effect for selected card */}
Expand All @@ -71,7 +77,7 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
{/* Main content area with padding */}
<div className="flex-1 p-4 pb-2">
{/* Title section */}
<div className="flex items-center justify-center mb-4 min-h-[48px]">
<div className="flex flex-col items-center justify-center mb-4 min-h-[48px]">
<h3
className={cn(
"font-medium text-center leading-tight line-clamp-2 transition-all duration-300",
Expand All @@ -84,6 +90,7 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
>
{project.title}
</h3>
<OptimisticIndicator isOptimistic={optimistic} className="mt-1" />
</div>

{/* Task count pills */}
Expand Down
52 changes: 24 additions & 28 deletions archon-ui-main/src/features/projects/hooks/useProjectQueries.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { createOptimisticEntity, replaceOptimisticEntity, removeDuplicateEntities, type OptimisticEntity } from "@/features/shared/optimistic";
import { DISABLED_QUERY_KEY, STALE_TIMES } from "../../shared/queryPatterns";
import { useSmartPolling } from "../../ui/hooks";
import { useToast } from "../../ui/hooks/useToast";
Expand Down Expand Up @@ -54,21 +55,20 @@ export function useCreateProject() {
// Snapshot the previous value
const previousProjects = queryClient.getQueryData<Project[]>(projectKeys.lists());

// Create optimistic project with temporary ID
const tempId = `temp-${Date.now()}`;
const optimisticProject: Project = {
id: tempId, // Temporary ID until real one comes back
title: newProjectData.title,
description: newProjectData.description,
github_repo: newProjectData.github_repo,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
prd: undefined,
features: [],
data: undefined,
docs: [],
pinned: false,
};
// Create optimistic project with stable ID
const optimisticProject = createOptimisticEntity<Project>(
{
title: newProjectData.title,
description: newProjectData.description,
github_repo: newProjectData.github_repo,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
docs: [],
features: [],
prd: null,
data_schema: null,
}
);

// Optimistically add the new project
queryClient.setQueryData(projectKeys.lists(), (old: Project[] | undefined) => {
Expand All @@ -77,7 +77,7 @@ export function useCreateProject() {
return [optimisticProject, ...old];
});

return { previousProjects, tempId };
return { previousProjects, optimisticId: optimisticProject._localId };
},
onError: (error, variables, context) => {
const errorMessage = error instanceof Error ? error.message : String(error);
Expand All @@ -94,18 +94,14 @@ export function useCreateProject() {
// Extract the actual project from the response
const newProject = response.project;

// Replace optimistic project with real one from server
queryClient.setQueryData(projectKeys.lists(), (old: Project[] | undefined) => {
if (!old) return [newProject];
// Replace only the specific temp project with real one
return old
.map((project) => (project.id === context?.tempId ? newProject : project))
.filter(
(project, index, self) =>
// Remove any duplicates just in case
index === self.findIndex((p) => p.id === project.id),
);
});
// Replace optimistic with server data
queryClient.setQueryData(
projectKeys.lists(),
(projects: (Project & Partial<OptimisticEntity>)[] = []) => {
const replaced = replaceOptimisticEntity(projects, context?.optimisticId || "", newProject);
return removeDuplicateEntities(replaced);
}
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Make replacement robust if the optimistic item isn’t found; also strongly type the mutation.

Fallback-insert the server entity when _localId isn’t matched to avoid losing the created project until refetch completes. Also, please type the mutation to the exact return type of projectService.createProject to prevent shape drift (e.g., Project vs { project: Project }).

-      queryClient.setQueryData(
-        projectKeys.lists(),
-        (projects: (Project & Partial<OptimisticEntity>)[] = []) => {
-          const replaced = replaceOptimisticEntity(projects, context?.optimisticId || "", newProject);
-          return removeDuplicateEntities(replaced);
-        }
-      );
+      queryClient.setQueryData(
+        projectKeys.lists(),
+        (projects: (Project & Partial<OptimisticEntity>)[] = []) => {
+          const localId = context?.optimisticId ?? "";
+          const replaced = replaceOptimisticEntity(projects, localId, newProject);
+          const hasServer = replaced.some((p) => p.id === newProject.id);
+          const next = hasServer ? replaced : [newProject, ...replaced];
+          return removeDuplicateEntities(next);
+        },
+      );

If createProject returns { project: Project }, type the mutation accordingly; otherwise prefer Project:

-  return useMutation({
+  return useMutation<Project, Error, CreateProjectRequest, { previousProjects?: Project[]; optimisticId: string }>({
     mutationFn: (projectData: CreateProjectRequest) => projectService.createProject(projectData),

Run to confirm the service return shape and catch any legacy temp-ID code:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect createProject signature/return usage
rg -n -C3 --type=ts 'createProject\s*\(' archon-ui-main/src/features/projects

# Ensure no leftover temp IDs or Date.now()-based IDs in UI code
rg -nP --type=ts --type=tsx '(temp-|Date\.now\s*\()' archon-ui-main/src

Length of output: 1453


Type mutation to service response & fallback-insert server project when optimistic replacement misses

projectService.createProject returns { project_id: string; project: Project; status: string } — type the mutation to that exact shape (or use Awaited<ReturnType>) and, in onSuccess, use the wrapped project (data.project) and fallback-insert it into the cached list if replaceOptimisticEntity didn't match the optimistic _localId (prevents losing a created project until refetch).

Files: archon-ui-main/src/features/projects/hooks/useProjectQueries.ts (mutation / setQueryData) and archon-ui-main/src/features/projects/services/projectService.ts (createProject return shape)

Suggested changes:

  1. Make setQueryData robust (use server project, insert when optimistic not found):
-      queryClient.setQueryData(
-        projectKeys.lists(),
-        (projects: (Project & Partial<OptimisticEntity>)[] = []) => {
-          const replaced = replaceOptimisticEntity(projects, context?.optimisticId || "", newProject);
-          return removeDuplicateEntities(replaced);
-        }
-      );
+      // `data` is the mutation response; handle both raw Project or { project: Project } shapes
+      const serverProject = (data as any)?.project ?? (data as any);
+      queryClient.setQueryData(
+        projectKeys.lists(),
+        (projects: (Project & Partial<OptimisticEntity>)[] = []) => {
+          const localId = context?.optimisticId ?? "";
+          const replaced = replaceOptimisticEntity(projects, localId, serverProject);
+          const hasServer = replaced.some((p) => p.id === serverProject.id);
+          const next = hasServer ? replaced : [serverProject, ...replaced];
+          return removeDuplicateEntities(next);
+        },
+      );
  1. Strongly type the mutation to the service return shape:
-  return useMutation({
+  return useMutation<
+    Awaited<ReturnType<typeof projectService.createProject>>,
+    Error,
+    CreateProjectRequest,
+    { previousProjects?: Project[]; optimisticId: string }
+  >({
     mutationFn: (projectData: CreateProjectRequest) => projectService.createProject(projectData),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Replace optimistic with server data
queryClient.setQueryData(
projectKeys.lists(),
(projects: (Project & Partial<OptimisticEntity>)[] = []) => {
const replaced = replaceOptimisticEntity(projects, context?.optimisticId || "", newProject);
return removeDuplicateEntities(replaced);
}
);
// `data` is the mutation response; handle both raw Project or { project: Project } shapes
const serverProject = (data as any)?.project ?? (data as any);
queryClient.setQueryData(
projectKeys.lists(),
(projects: (Project & Partial<OptimisticEntity>)[] = []) => {
const localId = context?.optimisticId ?? "";
const replaced = replaceOptimisticEntity(projects, localId, serverProject);
const hasServer = replaced.some((p) => p.id === serverProject.id);
const next = hasServer ? replaced : [serverProject, ...replaced];
return removeDuplicateEntities(next);
},
);
🤖 Prompt for AI Agents
In archon-ui-main/src/features/projects/hooks/useProjectQueries.ts around lines
97-104, the onSuccess handler is using newProject without typing the mutation to
the actual service response and can drop a created project if
replaceOptimisticEntity doesn't find the optimistic _localId; change the
mutation generic to the exact return shape (use Awaited<ReturnType<typeof
projectService.createProject>> or an explicit { project_id: string; project:
Project; status: string }), then in onSuccess read the server object from
data.project and when calling queryClient.setQueryData, run
replaceOptimisticEntity and if that replacement did not modify any entity (i.e.,
optimistic not found) push/insert the server project into the projects array
before deduping with removeDuplicateEntities so the created project is not lost.


showToast("Project created successfully!", "success");
},
Expand Down
12 changes: 10 additions & 2 deletions archon-ui-main/src/features/projects/tasks/components/TaskCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Tag } from "lucide-react";
import type React from "react";
import { useCallback } from "react";
import { useDrag, useDrop } from "react-dnd";
import { isOptimistic } from "../../../shared/optimistic";
import { OptimisticIndicator } from "../../../ui/primitives/OptimisticIndicator";
import { useTaskActions } from "../hooks";
import type { Assignee, Task, TaskPriority } from "../types";
import { getOrderColor, getOrderGlow, ItemTypes } from "../utils/task-styles";
Expand Down Expand Up @@ -34,6 +36,9 @@ export const TaskCard: React.FC<TaskCardProps> = ({
selectedTasks,
onTaskSelect,
}) => {
// Check if task is optimistic
const optimistic = isOptimistic(task);

// Use business logic hook with changePriority
const { changeAssignee, changePriority, isUpdating } = useTaskActions(projectId);

Expand Down Expand Up @@ -152,7 +157,7 @@ export const TaskCard: React.FC<TaskCardProps> = ({
}}
>
<div
className={`${cardBaseStyles} ${transitionStyles} ${hoverEffectClasses} ${highlightGlow} ${selectionGlow} w-full min-h-[140px] h-full`}
className={`${cardBaseStyles} ${transitionStyles} ${hoverEffectClasses} ${highlightGlow} ${selectionGlow} ${optimistic ? "opacity-80 ring-1 ring-cyan-400/30" : ""} w-full min-h-[140px] h-full`}
>
{/* Priority indicator with beautiful glow */}
<div
Expand All @@ -177,8 +182,11 @@ export const TaskCard: React.FC<TaskCardProps> = ({
</div>
)}

{/* Optimistic indicator */}
<OptimisticIndicator isOptimistic={optimistic} className="ml-auto" />

{/* Action buttons group */}
<div className="ml-auto flex items-center gap-1.5">
<div className={`${optimistic ? "" : "ml-auto"} flex items-center gap-1.5`}>
<TaskCardActions
taskId={task.id}
taskTitle={task.title}
Expand Down
57 changes: 31 additions & 26 deletions archon-ui-main/src/features/projects/tasks/hooks/useTaskQueries.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { createOptimisticEntity, replaceOptimisticEntity, removeDuplicateEntities, type OptimisticEntity } from "@/features/shared/optimistic";
import { DISABLED_QUERY_KEY, STALE_TIMES } from "../../../shared/queryPatterns";
import { useSmartPolling } from "../../../ui/hooks";
import { useToast } from "../../../ui/hooks/useToast";
Expand Down Expand Up @@ -55,26 +56,29 @@ export function useCreateTask() {
// Snapshot the previous value
const previousTasks = queryClient.getQueryData<Task[]>(taskKeys.byProject(newTaskData.project_id));

// Create optimistic task with temporary ID
const tempId = `temp-${Date.now()}`;
const optimisticTask: Task = {
id: tempId, // Temporary ID until real one comes back
...newTaskData,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
// Ensure all required fields have defaults (let backend handle assignee default)
task_order: newTaskData.task_order ?? 100,
status: newTaskData.status ?? "todo",
assignee: newTaskData.assignee ?? "User", // Keep for now as UI needs a value for optimistic update
} as Task;
// Create optimistic task with stable ID
const optimisticTask = createOptimisticEntity<Task>(
{
project_id: newTaskData.project_id,
title: newTaskData.title,
description: newTaskData.description || "",
status: newTaskData.status ?? "todo",
assignee: newTaskData.assignee ?? "User",
feature: newTaskData.feature,
task_order: newTaskData.task_order ?? 100,
priority: newTaskData.priority ?? "medium",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
}
);

// Optimistically add the new task
queryClient.setQueryData(taskKeys.byProject(newTaskData.project_id), (old: Task[] | undefined) => {
if (!old) return [optimisticTask];
return [...old, optimisticTask];
});

return { previousTasks, tempId };
return { previousTasks, optimisticId: optimisticTask._localId };
},
onError: (error, variables, context) => {
const errorMessage = error instanceof Error ? error.message : String(error);
Expand All @@ -85,20 +89,21 @@ export function useCreateTask() {
}
showToast(`Failed to create task: ${errorMessage}`, "error");
},
onSuccess: (data, variables, context) => {
// Replace optimistic task with real one from server
queryClient.setQueryData(taskKeys.byProject(variables.project_id), (old: Task[] | undefined) => {
if (!old) return [data];
// Replace only the specific temp task with real one
return old
.map((task) => (task.id === context?.tempId ? data : task))
.filter(
(task, index, self) =>
// Remove any duplicates just in case
index === self.findIndex((t) => t.id === task.id),
);
onSuccess: (serverTask, variables, context) => {
// Replace optimistic with server data
queryClient.setQueryData(
taskKeys.byProject(variables.project_id),
(tasks: (Task & Partial<OptimisticEntity>)[] = []) => {
const replaced = replaceOptimisticEntity(tasks, context?.optimisticId || "", serverTask);
return removeDuplicateEntities(replaced);
}
);

// Invalidate counts since we have a new task
queryClient.invalidateQueries({
queryKey: taskKeys.counts(),
});
queryClient.invalidateQueries({ queryKey: taskKeys.counts() });

showToast("Task created successfully", "success");
},
onSettled: (_data, _error, variables) => {
Expand Down
Loading