Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
12ff6e1
feat: initial work on new logs component
mrkaye97 Jan 20, 2026
6d93538
feat: improve autocomplete, refactor a bit
mrkaye97 Jan 20, 2026
3cebb09
refactor: remove some unused stuff
mrkaye97 Jan 20, 2026
083dc9f
refactor: more cleanup
mrkaye97 Jan 20, 2026
8b7c8ac
fix: on click handler
mrkaye97 Jan 20, 2026
d35b598
refactor: remove duped data, do some cleanup
mrkaye97 Jan 20, 2026
077bd0f
chore: simplify a bunch
mrkaye97 Jan 20, 2026
df56520
fix: pass level through
mrkaye97 Jan 20, 2026
6ae5da8
chore: cleanup
mrkaye97 Jan 20, 2026
871f0c4
fix: tsc
mrkaye97 Jan 20, 2026
a7f2945
chore: remove a bunch more stuff
mrkaye97 Jan 20, 2026
3116508
chore: comments
mrkaye97 Jan 20, 2026
c730261
fix: revert existing log component, add new filterable one
mrkaye97 Jan 20, 2026
3fef744
refactor: create use logs hook, simplify step-run-logs.tsx
mrkaye97 Jan 20, 2026
e21d176
chore: rename
mrkaye97 Jan 20, 2026
5743f69
chore: rename
mrkaye97 Jan 20, 2026
0d5c5a1
chore: fix tsc
mrkaye97 Jan 20, 2026
6c0cc17
feat: add a flag to enable the new experience
mrkaye97 Jan 20, 2026
dd4a16d
chore: lint
mrkaye97 Jan 20, 2026
3bd1dac
feat: provider
mrkaye97 Jan 21, 2026
ce6d73c
chore: lint
mrkaye97 Jan 21, 2026
8efd75e
fix: cursor jumping
mrkaye97 Jan 21, 2026
109305c
feat: try to use ghostty-web for rendering log lines instead of ansiT…
abelanger5 Jan 21, 2026
8efa791
fix: start getting the log viewer scrolling properly again
mrkaye97 Jan 22, 2026
47ce67f
fix: more scroll improvements
mrkaye97 Jan 22, 2026
c1fbf9e
feat: replace ghostty
mrkaye97 Jan 22, 2026
97d9424
chore: cleanup
mrkaye97 Jan 22, 2026
3655d9c
fix: dynamic height and width
mrkaye97 Jan 22, 2026
0d11e92
chore: lint
mrkaye97 Jan 22, 2026
42d0728
chore: format css
mrkaye97 Jan 22, 2026
0b22da8
chore: remove unused ref and styles
mrkaye97 Jan 22, 2026
0258c39
chore: cleanup
mrkaye97 Jan 22, 2026
81e6baa
fix: terminal styling
mrkaye97 Jan 22, 2026
40648f3
fix: scroll behavior / polling behavior while tasks are running still
mrkaye97 Jan 22, 2026
ff93c81
fix: jump to top on search change
mrkaye97 Jan 22, 2026
dc04df7
fix: improve loading / empty states
mrkaye97 Jan 22, 2026
9de8f90
chore: lint
mrkaye97 Jan 22, 2026
95f5262
Merge branch 'main' into mk/logs-fe-part-i
mrkaye97 Jan 22, 2026
8f92c1b
fix: status
mrkaye97 Jan 22, 2026
6256e5b
Merge branch 'mk/logs-fe-part-i' of https://github.com/hatchet-dev/ha…
mrkaye97 Jan 22, 2026
1102226
fix: enter to submit search
mrkaye97 Jan 22, 2026
1ccf925
fix: remove color var to fix ansi highlighting
mrkaye97 Jan 22, 2026
429347e
feat: expand / collapse on click
mrkaye97 Jan 22, 2026
7d089f2
chore: rm unused code
mrkaye97 Jan 22, 2026
c0376ac
Add edge detection for scroll callbacks to prevent repeated API calls…
Copilot Jan 22, 2026
7de68a2
Fix polling loop to prevent overlapping requests in log search (#2845)
Copilot Jan 22, 2026
4263513
chore: lint
mrkaye97 Jan 22, 2026
e85be47
Merge branch 'main' into mk/logs-fe-part-i
mrkaye97 Jan 29, 2026
8206a8c
fix: dedupe log lines
mrkaye97 Jan 29, 2026
cf174ea
fix: remove duped component
mrkaye97 Jan 29, 2026
3605cc8
fix: pass order by direction
mrkaye97 Jan 29, 2026
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
1 change: 1 addition & 0 deletions frontend/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@heroicons/react": "^2.2.0",
"@hookform/resolvers": "^3.10.0",
"@lukemorales/query-key-factory": "^1.3.4",
"@melloware/react-logviewer": "^6.3.5",
"@monaco-editor/react": "^4.7.0",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-avatar": "^1.1.3",
Expand Down
69 changes: 69 additions & 0 deletions frontend/app/pnpm-lock.yaml

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

117 changes: 117 additions & 0 deletions frontend/app/src/components/v1/cloud/logging/components/Terminal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { cn } from '@/lib/utils';
import { LazyLog, ScrollFollow } from '@melloware/react-logviewer';
import { useCallback, useRef } from 'react';

interface TerminalProps {
logs: string;
autoScroll?: boolean;
onScrollToTop?: () => void;
onScrollToBottom?: () => void;
onAtTopChange?: (atTop: boolean) => void;
className?: string;
}

function Terminal({
logs,
autoScroll = false,
onScrollToTop,
onScrollToBottom,
onAtTopChange,
className,
}: TerminalProps) {
const lastScrollTopRef = useRef(0);
const wasAtTopRef = useRef(true);
const wasInTopRegionRef = useRef(false);
const wasInBottomRegionRef = useRef(false);

const handleLineClick = useCallback(
(event: React.MouseEvent<HTMLSpanElement>) => {
const lineElement = (event.target as HTMLElement).closest('.log-line');
if (lineElement) {
lineElement.classList.toggle('expanded');
}
},
[],
);

const handleScroll = useCallback(
({
scrollTop,
scrollHeight,
clientHeight,
}: {
scrollTop: number;
scrollHeight: number;
clientHeight: number;
}) => {
const scrollableHeight = scrollHeight - clientHeight;
if (scrollableHeight <= 0) {
return;
}

const scrollPercentage = scrollTop / scrollableHeight;
const isScrollingUp = scrollTop < lastScrollTopRef.current;
const isScrollingDown = scrollTop > lastScrollTopRef.current;

const isAtTop = scrollPercentage < 0.05;
if (onAtTopChange && isAtTop !== wasAtTopRef.current) {
wasAtTopRef.current = isAtTop;
onAtTopChange(isAtTop);
}

// Near top (newest logs with newest-first) - for running tasks
// Only fire when entering the region (edge detection)
// The region is defined by both scroll direction AND position, so changing
// direction automatically resets the region state
const isInTopRegion = isScrollingUp && scrollPercentage < 0.3;
if (isInTopRegion && !wasInTopRegionRef.current && onScrollToTop) {
onScrollToTop();
}
wasInTopRegionRef.current = isInTopRegion;

// Near bottom (older logs with newest-first) - for infinite scroll
// Only fire when entering the region (edge detection)
// The region is defined by both scroll direction AND position, so changing
// direction automatically resets the region state
const isInBottomRegion = isScrollingDown && scrollPercentage > 0.7;
if (
isInBottomRegion &&
!wasInBottomRegionRef.current &&
onScrollToBottom
) {
onScrollToBottom();
}
wasInBottomRegionRef.current = isInBottomRegion;

lastScrollTopRef.current = scrollTop;
},
[onScrollToTop, onScrollToBottom, onAtTopChange],
);

return (
<div
className={cn(
'terminal-root h-[500px] md:h-[600px] rounded-md w-full overflow-hidden',
className,
)}
>
<ScrollFollow
startFollowing={autoScroll}
render={({ follow, onScroll }) => (
<LazyLog
text={logs}
follow={follow}
onScroll={(args) => {
onScroll(args);
handleScroll(args);
}}
onLineContentClick={handleLineClick}
selectableLines
/>
)}
/>
</div>
);
}

export default Terminal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { AutocompleteSuggestion, LOG_LEVELS } from './types';

const LEVEL_DESCRIPTIONS: Record<string, string> = {
error: 'Error messages',
warn: 'Warning messages',
info: 'Informational messages',
debug: 'Debug messages',
};

export type AutocompleteMode = 'key' | 'value' | 'none';

export interface AutocompleteState {
mode: AutocompleteMode;
suggestions: AutocompleteSuggestion[];
}

export function getAutocomplete(query: string): AutocompleteState {
const trimmed = query.trimEnd();
const lastWord = trimmed.split(' ').pop() || '';

if (lastWord.startsWith('level:')) {
const partial = lastWord.slice(6).toLowerCase();
const suggestions = LOG_LEVELS.filter((level) =>
level.startsWith(partial),
).map((level) => ({
type: 'value' as const,
label: level,
value: level,
description: LEVEL_DESCRIPTIONS[level],
}));
return { mode: 'value', suggestions };
}

if ('level:'.startsWith(lastWord.toLowerCase()) && lastWord.length > 0) {
return {
mode: 'key',
suggestions: [
{
type: 'key',
label: 'level',
value: 'level:',
description: 'Filter by log level',
},
],
};
}

if (trimmed === '' || query.endsWith(' ')) {
return {
mode: 'key',
suggestions: [
{
type: 'key',
label: 'level',
value: 'level:',
description: 'Filter by log level',
},
],
};
}

return { mode: 'none', suggestions: [] };
}

export function applySuggestion(
query: string,
suggestion: AutocompleteSuggestion,
): string {
const trimmed = query.trimEnd();
const words = trimmed.split(' ');
const lastWord = words.pop() || '';

if (suggestion.type === 'value') {
const prefix = lastWord.slice(0, lastWord.indexOf(':') + 1);
words.push(prefix + suggestion.value);
} else {
if (lastWord && 'level:'.startsWith(lastWord.toLowerCase())) {
words.push(suggestion.value);
} else {
words.push(lastWord, suggestion.value);
}
}

return words.filter(Boolean).join(' ');
}
Loading
Loading