Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ on:

jobs:
claude-review:
if: github.event.pull_request.draft == false
if: |
github.event.pull_request.draft == false &&
github.actor != 'graphite-app[bot]' &&
github.actor != 'dependabot[bot]'

runs-on: ubuntu-latest
permissions:
Expand Down
63 changes: 62 additions & 1 deletion apps/web/app/(app)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,66 @@ export default function NewPage() {
const resetDraft = useQuickNoteDraftReset(selectedProject)
const { draft: quickNoteDraft } = useQuickNoteDraft(selectedProject || "")

const { noteMutation } = useDocumentMutations({
const { noteMutation, bulkDeleteMutation } = useDocumentMutations({
onClose: () => {
resetDraft()
setIsFullscreen(false)
},
})

const [selectedDocumentIds, setSelectedDocumentIds] = useState<Set<string>>(
new Set(),
)
const [isSelectionMode, setIsSelectionMode] = useState(false)

const handleToggleSelection = useCallback((documentId: string) => {
setSelectedDocumentIds((prev) => {
const next = new Set(prev)
if (next.has(documentId)) {
next.delete(documentId)
} else {
next.add(documentId)
}
return next
})
}, [])

const handleClearSelection = useCallback(() => {
setSelectedDocumentIds(new Set())
setIsSelectionMode(false)
}, [])

const handleEnterSelectionMode = useCallback(() => {
setIsSelectionMode(true)
}, [])

const handleSelectAllVisible = useCallback((visibleIds: string[]) => {
setSelectedDocumentIds((prev) => {
const next = new Set(prev)
for (const id of visibleIds) {
next.add(id)
}
return next
})
}, [])

const handleBulkDelete = useCallback(() => {
const ids = Array.from(selectedDocumentIds)
if (ids.length === 0) return
bulkDeleteMutation.mutate(
{ documentIds: ids },
{
onSuccess: () => {
setSelectedDocumentIds(new Set())
setIsSelectionMode(false)
if (selectedDocument && ids.includes(selectedDocument.id ?? "")) {
setDocId(null)
}
},
},
)
}, [selectedDocumentIds, bulkDeleteMutation, selectedDocument, setDocId])

type SpaceHighlightsResponse = {
highlights: HighlightItem[]
questions: string[]
Expand Down Expand Up @@ -349,6 +402,14 @@ export default function NewPage() {
<MemoriesGrid
isChatOpen={chatOpen}
onOpenDocument={handleOpenDocument}
isSelectionMode={isSelectionMode}
selectedDocumentIds={selectedDocumentIds}
onEnterSelectionMode={handleEnterSelectionMode}
onToggleSelection={handleToggleSelection}
onClearSelection={handleClearSelection}
onSelectAllVisible={handleSelectAllVisible}
onBulkDelete={handleBulkDelete}
isBulkDeleting={bulkDeleteMutation.isPending}
quickNoteProps={{
onSave: handleQuickNoteSave,
onMaximize: handleMaximize,
Expand Down
8 changes: 6 additions & 2 deletions apps/web/app/upgrade-mcp/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,9 @@ export default function MigrateMCPPage() {
className="bg-white/5 border-white/10 text-white placeholder:text-slate-500 focus:border-blue-500/50 focus:ring-blue-500/20 transition-all duration-200 pl-4 pr-4 py-3 rounded-xl"
disabled={migrateMutation.isPending}
id="mcpUrl"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setMcpUrl(e.target.value)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setMcpUrl(e.target.value)
}
placeholder="https://mcp.supermemory.ai/userId/sse"
type="url"
value={mcpUrl}
Expand All @@ -205,7 +207,9 @@ export default function MigrateMCPPage() {
className="bg-white/5 border-white/10 text-white placeholder:text-slate-500 focus:border-blue-500/50 focus:ring-blue-500/20 transition-all duration-200 pl-4 pr-4 py-3 rounded-xl"
disabled={migrateMutation.isPending}
id="projectId"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setProjectId(e.target.value)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setProjectId(e.target.value)
}
placeholder="Project ID (default: 'default')"
type="text"
value={projectId}
Expand Down
176 changes: 88 additions & 88 deletions apps/web/components/chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -675,104 +675,104 @@ export function ChatSidebar({
<HistoryIcon className="size-4 text-[#737373]" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-lg bg-[#0A0E14] border-[#17181AB2] text-white">
<DialogHeader className="pb-4 border-b border-[#17181AB2]">
<DialogTitle>Chat History</DialogTitle>
<DialogDescription className="text-[#737373]">
Project: {selectedProject}
</DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-96">
{isLoadingThreads ? (
<div className="flex items-center justify-center py-8">
<SuperLoader label="Loading..." />
</div>
) : threads.length === 0 ? (
<div className="text-sm text-[#737373] text-center py-8">
No conversations yet
</div>
) : (
<div className="flex flex-col gap-1">
{threads.map((thread) => {
const isActive = thread.id === currentChatId
return (
<button
key={thread.id}
type="button"
onClick={() => loadThread(thread.id)}
className={cn(
"flex items-center justify-between rounded-md px-3 py-2 w-full text-left transition-colors",
isActive
? "bg-[#267BF1]/10"
: "hover:bg-[#17181A]",
)}
>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium truncate">
{thread.title || "Untitled Chat"}
</div>
<div className="text-xs text-[#737373]">
{formatRelativeTime(thread.updatedAt)}
</div>
<DialogContent className="sm:max-w-lg bg-[#0A0E14] border-[#17181AB2] text-white">
<DialogHeader className="pb-4 border-b border-[#17181AB2]">
<DialogTitle>Chat History</DialogTitle>
<DialogDescription className="text-[#737373]">
Project: {selectedProject}
</DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-96">
{isLoadingThreads ? (
<div className="flex items-center justify-center py-8">
<SuperLoader label="Loading..." />
</div>
) : threads.length === 0 ? (
<div className="text-sm text-[#737373] text-center py-8">
No conversations yet
</div>
) : (
<div className="flex flex-col gap-1">
{threads.map((thread) => {
const isActive = thread.id === currentChatId
return (
<button
key={thread.id}
type="button"
onClick={() => loadThread(thread.id)}
className={cn(
"flex items-center justify-between rounded-md px-3 py-2 w-full text-left transition-colors",
isActive
? "bg-[#267BF1]/10"
: "hover:bg-[#17181A]",
)}
>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium truncate">
{thread.title || "Untitled Chat"}
</div>
{confirmingDeleteId === thread.id ? (
<div className="flex items-center gap-1 ml-2">
<Button
type="button"
size="icon"
onClick={(e) => {
e.stopPropagation()
deleteThread(thread.id)
}}
className="bg-red-500 text-white hover:bg-red-600 h-7 w-7"
>
<Check className="size-3" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation()
setConfirmingDeleteId(null)
}}
className="h-7 w-7"
>
<XIcon className="size-3 text-[#737373]" />
</Button>
</div>
) : (
<div className="text-xs text-[#737373]">
{formatRelativeTime(thread.updatedAt)}
</div>
</div>
{confirmingDeleteId === thread.id ? (
<div className="flex items-center gap-1 ml-2">
<Button
type="button"
size="icon"
onClick={(e) => {
e.stopPropagation()
deleteThread(thread.id)
}}
className="bg-red-500 text-white hover:bg-red-600 h-7 w-7"
>
<Check className="size-3" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation()
setConfirmingDeleteId(thread.id)
setConfirmingDeleteId(null)
}}
className="h-7 w-7 ml-2"
className="h-7 w-7"
>
<Trash2 className="size-3 text-[#737373]" />
<XIcon className="size-3 text-[#737373]" />
</Button>
)}
</button>
)
})}
</div>
)}
</ScrollArea>
<Button
variant="outline"
className="w-full border-dashed border-[#73737333] bg-transparent hover:bg-[#17181A]"
onClick={() => {
handleNewChat()
setIsHistoryOpen(false)
}}
>
<Plus className="size-4 mr-1" /> New Conversation
</Button>
</DialogContent>
</Dialog>
</div>
) : (
<Button
type="button"
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation()
setConfirmingDeleteId(thread.id)
}}
className="h-7 w-7 ml-2"
>
<Trash2 className="size-3 text-[#737373]" />
</Button>
)}
</button>
)
})}
</div>
)}
</ScrollArea>
<Button
variant="outline"
className="w-full border-dashed border-[#73737333] bg-transparent hover:bg-[#17181A]"
onClick={() => {
handleNewChat()
setIsHistoryOpen(false)
}}
>
<Plus className="size-4 mr-1" /> New Conversation
</Button>
</DialogContent>
</Dialog>
<Button
variant="headers"
className="rounded-full text-base gap-3 h-10! border-[#73737333] bg-[#0D121A] cursor-pointer"
Expand Down
Loading
Loading