33import React from "react" ;
44import { Tabs , TabsContent , TabsList , TabsTrigger } from "@/components/ui/tabs" ;
55import { Button } from "@/components/ui/button" ;
6+ import { Checkbox } from "@/components/ui/checkbox" ;
67import {
78 CaretDownIcon ,
89 CaretUpIcon ,
@@ -25,6 +26,7 @@ import type grida from "@grida/schema";
2526import { useCurrentEditor , useEditorState } from "../use-editor" ;
2627import { useRecorder } from "../plugins/use-recorder" ;
2728import { saveAs } from "file-saver" ;
29+ import { editor } from "@/grida-canvas/editor.i" ;
2830
2931export function DevtoolsPanel ( ) {
3032 const expandable = useDialogState ( ) ;
@@ -75,6 +77,13 @@ export function DevtoolsPanel() {
7577 >
7678 Recorder
7779 </ TabsTrigger >
80+ < TabsTrigger
81+ onClick = { onTabClick }
82+ value = "events"
83+ className = "text-xs uppercase"
84+ >
85+ Events
86+ </ TabsTrigger >
7887 </ TabsList >
7988 </ div >
8089 < CollapsibleTrigger asChild >
@@ -145,6 +154,9 @@ export function DevtoolsPanel() {
145154 < TabsContent value = "recorder" className = "h-full" >
146155 < RecorderPanel />
147156 </ TabsContent >
157+ < TabsContent value = "events" className = "h-full" >
158+ < EventsPanel />
159+ </ TabsContent >
148160 </ CollapsibleContent >
149161 </ Tabs >
150162 </ Collapsible >
@@ -333,3 +345,163 @@ function RecorderPanel() {
333345 </ div >
334346 ) ;
335347}
348+
349+ type PatchEvent = {
350+ timestamp : number ;
351+ patches : editor . history . Patch [ ] ;
352+ action ?: any ;
353+ } ;
354+
355+ function EventsPanel ( ) {
356+ const currentEditor = useCurrentEditor ( ) ;
357+ const [ events , setEvents ] = React . useState < PatchEvent [ ] > ( [ ] ) ;
358+ const [ showNonDocumentPatches , setShowNonDocumentPatches ] =
359+ React . useState ( false ) ;
360+
361+ React . useEffect ( ( ) => {
362+ // Subscribe to document changes with selector
363+ const unsubscribe = currentEditor . doc . subscribeWithSelector (
364+ ( state ) => state . document ,
365+ ( doc , next , prev , action , patches ) => {
366+ if ( ! patches || patches . length === 0 ) return ;
367+
368+ setEvents ( ( prevEvents ) => {
369+ const newEvent : PatchEvent = {
370+ timestamp : Date . now ( ) ,
371+ patches,
372+ action,
373+ } ;
374+ // Keep only the last 50 events
375+ const updated = [ newEvent , ...prevEvents ] ;
376+ return updated . slice ( 0 , 50 ) ;
377+ } ) ;
378+ }
379+ ) ;
380+
381+ return ( ) => {
382+ unsubscribe ( ) ;
383+ } ;
384+ } , [ currentEditor ] ) ;
385+
386+ // Filter patches within events based on toggle
387+ const filteredEvents = React . useMemo ( ( ) => {
388+ return events
389+ . map ( ( event ) => {
390+ const filteredPatches = showNonDocumentPatches
391+ ? event . patches
392+ : event . patches . filter ( ( patch ) => patch . path [ 0 ] === "document" ) ;
393+
394+ return {
395+ ...event ,
396+ patches : filteredPatches ,
397+ } ;
398+ } )
399+ . filter ( ( event ) => event . patches . length > 0 ) ; // Only show events that have patches after filtering
400+ } , [ events , showNonDocumentPatches ] ) ;
401+
402+ return (
403+ < div className = "h-full overflow-y-auto" >
404+ < div className = "p-4" >
405+ < div className = "flex items-center justify-between mb-4" >
406+ < h3 className = "text-sm font-medium" >
407+ Recent Patches ({ filteredEvents . length } /50)
408+ </ h3 >
409+ < div className = "flex items-center gap-4" >
410+ < div className = "flex items-center gap-2" >
411+ < Checkbox
412+ id = "show-non-document-patches"
413+ checked = { showNonDocumentPatches }
414+ onCheckedChange = { ( checked ) =>
415+ setShowNonDocumentPatches ( checked === true )
416+ }
417+ />
418+ < label
419+ htmlFor = "show-non-document-patches"
420+ className = "text-xs text-muted-foreground cursor-pointer select-none"
421+ >
422+ Show non-document patches
423+ </ label >
424+ </ div >
425+ < Button
426+ variant = "ghost"
427+ size = "sm"
428+ onClick = { ( ) => setEvents ( [ ] ) }
429+ disabled = { events . length === 0 }
430+ >
431+ < TrashIcon className = "w-4 h-4" />
432+ </ Button >
433+ </ div >
434+ </ div >
435+ { filteredEvents . length === 0 ? (
436+ < div className = "text-sm text-muted-foreground text-center py-8" >
437+ No patches yet. Make changes to see them here.
438+ </ div >
439+ ) : (
440+ < div className = "space-y-2" >
441+ { filteredEvents . map ( ( event , index ) => (
442+ < Collapsible key = { index } >
443+ < div className = "border rounded-lg p-3" >
444+ < CollapsibleTrigger className = "w-full" >
445+ < div className = "flex items-center justify-between text-xs" >
446+ < div className = "flex items-center gap-2" >
447+ < span className = "font-mono text-muted-foreground" >
448+ #{ filteredEvents . length - index }
449+ </ span >
450+ < span className = "font-medium" >
451+ { event . action ?. type || "unknown" }
452+ </ span >
453+ < span className = "text-muted-foreground" >
454+ { event . patches . length } patch
455+ { event . patches . length !== 1 ? "es" : "" }
456+ </ span >
457+ </ div >
458+ < span className = "text-muted-foreground font-mono" >
459+ { event . timestamp }
460+ </ span >
461+ </ div >
462+ </ CollapsibleTrigger >
463+ < CollapsibleContent className = "mt-2" >
464+ < div className = "space-y-1" >
465+ { event . patches . map ( ( patch , patchIndex ) => (
466+ < div
467+ key = { patchIndex }
468+ className = "text-xs font-mono bg-muted p-2 rounded"
469+ >
470+ < div className = "flex items-start gap-2" >
471+ < span
472+ className = { `font-bold ${
473+ patch . op === "add"
474+ ? "text-green-600"
475+ : patch . op === "remove"
476+ ? "text-red-600"
477+ : "text-blue-600"
478+ } `}
479+ >
480+ { patch . op }
481+ </ span >
482+ < div className = "flex-1" >
483+ < div className = "text-muted-foreground" >
484+ { patch . path . join ( " → " ) }
485+ </ div >
486+ { patch . value !== undefined && (
487+ < div className = "mt-1 text-foreground" >
488+ { typeof patch . value === "object"
489+ ? JSON . stringify ( patch . value , null , 2 )
490+ : String ( patch . value ) }
491+ </ div >
492+ ) }
493+ </ div >
494+ </ div >
495+ </ div >
496+ ) ) }
497+ </ div >
498+ </ CollapsibleContent >
499+ </ div >
500+ </ Collapsible >
501+ ) ) }
502+ </ div >
503+ ) }
504+ </ div >
505+ </ div >
506+ ) ;
507+ }
0 commit comments