1818
1919import { memo , useMemo , useRef , useCallback , useEffect , useState , startTransition , useDeferredValue } from "react" ;
2020import { useShallow } from "zustand/react/shallow" ;
21- import { User , Cpu , ZoomIn , ZoomOut } from "lucide-react" ;
21+ import { User , Cpu , ZoomIn , ZoomOut , Download , ExternalLink , Tag , WrapText } from "lucide-react" ;
2222import { cn } from "@/lib/utils" ;
2323import { useFormattedHotkey , useModKey } from "@/hooks/use-hotkey-label" ;
2424import type { LogEntry , HistogramBucket } from "@/lib/api/log-adapter/types" ;
@@ -36,7 +36,7 @@ import {
3636 type TimelineContainerHandle ,
3737} from "@/components/log-viewer/components/timeline/components/timeline-container" ;
3838import { LogList , type LogListHandle } from "@/components/log-viewer/components/log-list" ;
39- import { Footer } from "@/components/log-viewer/components/footer " ;
39+ import { ScrollPinControl } from "@/components/log-viewer/components/scroll-pin-control " ;
4040import { LogViewerSkeleton } from "@/components/log-viewer/components/log-viewer-skeleton" ;
4141import { useLogViewerStore } from "@/components/log-viewer/store/log-viewer-store" ;
4242import { HISTOGRAM_BUCKET_JUMP_WINDOW_MS } from "@/components/log-viewer/lib/constants" ;
@@ -476,20 +476,117 @@ function LogViewerInner({ data, filter, timeline, className, showTimeline = true
476476 />
477477 ) }
478478
479- { /* Section 1: Filter bar — excluded from focus redirect so dropdown items work */ }
479+ { /* Section 1: Filter bar + Actions — excluded from focus redirect so dropdown items work */ }
480480 < div
481- className = "shrink-0 border-b p -2"
481+ className = "shrink-0 border-b px-3 py -2"
482482 data-no-focus-redirect
483483 >
484- < FilterBar
485- ref = { filterBarRef }
486- data = { rawEntries }
487- fields = { filterFields }
488- chips = { filterChips }
489- onChipsChange = { handleFilterChipsChange }
490- presets = { LOG_FILTER_PRESETS }
491- placeholder = { `Search logs (${ searchShortcut } )...` }
492- />
484+ < div className = "flex items-center gap-2" >
485+ < div className = "flex-1" >
486+ < FilterBar
487+ ref = { filterBarRef }
488+ data = { rawEntries }
489+ fields = { filterFields }
490+ chips = { filterChips }
491+ onChipsChange = { handleFilterChipsChange }
492+ presets = { LOG_FILTER_PRESETS }
493+ placeholder = { `Search logs (${ searchShortcut } )...` }
494+ />
495+ </ div >
496+
497+ { /* Action buttons */ }
498+ < div className = "flex shrink-0 items-center gap-1" >
499+ { /* Scroll/Pin controls */ }
500+ < ScrollPinControl
501+ isStreaming = { isStreaming ?? false }
502+ isPinned = { isPinnedToBottom }
503+ onScrollToBottom = { handleJumpToBottom }
504+ onTogglePin = { handleTogglePin }
505+ />
506+
507+ { /* Show task toggle (hidden when scoped to a single task) */ }
508+ { scope !== "task" && (
509+ < Tooltip >
510+ < TooltipTrigger asChild >
511+ < Button
512+ variant = "ghost"
513+ size = "icon-sm"
514+ onClick = { toggleShowTask }
515+ className = {
516+ showTask
517+ ? "bg-foreground text-background hover:bg-foreground hover:text-background dark:hover:bg-foreground dark:hover:text-background"
518+ : ""
519+ }
520+ aria-label = { `${ showTask ? "Hide" : "Show" } task` }
521+ aria-pressed = { showTask }
522+ >
523+ < Tag className = "size-4" />
524+ </ Button >
525+ </ TooltipTrigger >
526+ < TooltipContent side = "bottom" > { showTask ? "Hide" : "Show" } task</ TooltipContent >
527+ </ Tooltip >
528+ ) }
529+
530+ { /* Wrap lines toggle */ }
531+ < Tooltip >
532+ < TooltipTrigger asChild >
533+ < Button
534+ variant = "ghost"
535+ size = "icon-sm"
536+ onClick = { toggleWrapLines }
537+ className = {
538+ wrapLines
539+ ? "bg-foreground text-background hover:bg-foreground hover:text-background dark:hover:bg-foreground dark:hover:text-background"
540+ : ""
541+ }
542+ aria-label = { `${ wrapLines ? "Disable" : "Enable" } line wrap` }
543+ aria-pressed = { wrapLines }
544+ >
545+ < WrapText className = "size-4" />
546+ </ Button >
547+ </ TooltipTrigger >
548+ < TooltipContent side = "bottom" > { wrapLines ? "Disable" : "Enable" } line wrap</ TooltipContent >
549+ </ Tooltip >
550+
551+ { /* Download button */ }
552+ < Tooltip >
553+ < TooltipTrigger asChild >
554+ < Button
555+ variant = "ghost"
556+ size = "icon-sm"
557+ onClick = { handleDownload }
558+ aria-label = "Download logs"
559+ >
560+ < Download className = "size-4" />
561+ </ Button >
562+ </ TooltipTrigger >
563+ < TooltipContent side = "bottom" > Download logs</ TooltipContent >
564+ </ Tooltip >
565+
566+ { /* External link - opens raw logs in new tab */ }
567+ { externalLogUrl && (
568+ < Tooltip >
569+ < TooltipTrigger asChild >
570+ < Button
571+ variant = "ghost"
572+ size = "icon-sm"
573+ asChild
574+ aria-label = "Open raw logs in new tab"
575+ >
576+ < a
577+ href = { externalLogUrl }
578+ target = "_blank"
579+ rel = "noopener noreferrer"
580+ >
581+ < ExternalLink className = "size-4" />
582+ </ a >
583+ </ Button >
584+ </ TooltipTrigger >
585+ < TooltipContent side = "bottom" > Open raw logs in new tab</ TooltipContent >
586+ </ Tooltip >
587+ ) }
588+ </ div >
589+ </ div >
493590 </ div >
494591
495592 { /* Section 2: Timeline Histogram — excluded from focus redirect so draggers work */ }
@@ -584,22 +681,6 @@ function LogViewerInner({ data, filter, timeline, className, showTimeline = true
584681 hideTask = { scope === "task" }
585682 />
586683 </ div >
587-
588- { /* Section 4: Footer */ }
589- < div className = "shrink-0" >
590- < Footer
591- wrapLines = { wrapLines }
592- onToggleWrapLines = { toggleWrapLines }
593- showTask = { scope === "task" ? false : showTask }
594- onToggleShowTask = { scope === "task" ? undefined : toggleShowTask }
595- externalLogUrl = { externalLogUrl }
596- onDownload = { handleDownload }
597- isStreaming = { isStreaming }
598- isPinnedToBottom = { isPinnedToBottom }
599- onScrollToBottom = { handleJumpToBottom }
600- onTogglePinnedToBottom = { handleTogglePin }
601- />
602- </ div >
603684 </ div >
604685 ) ;
605686}
0 commit comments