Skip to content
Draft
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
16 changes: 10 additions & 6 deletions packages/cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ src/
│ ├── RowOverview.tsx # Structured overview (top-level attrs, event attrs, resource attrs)
│ ├── ColumnValues.tsx # Shared key-value renderer (used by Column Values tab + Event Details)
│ ├── LoginForm.tsx # Email/password login form (used inside TUI App)
│ └── SourcePicker.tsx # Arrow-key source selector
│ ├── SourcePicker.tsx # Arrow-key source selector
│ └── Spotlight.tsx # Ctrl+K spotlight overlay for quick navigation
└── utils/
├── config.ts # Session persistence (~/.config/hyperdx/cli/session.json)
├── editor.ts # $EDITOR integration for time range and select clause editing
Expand Down Expand Up @@ -157,6 +158,7 @@ Key expression mappings from the web frontend's `getConfig()`:
| `t` | Edit time range in $EDITOR |
| `f` | Toggle follow mode (live tail) |
| `w` | Toggle line wrap |
| `Ctrl+K` | Open spotlight (quick navigation) |
| `A` (Shift+A) | Open alerts page |
| `?` | Toggle help screen |
| `q` | Quit |
Expand Down Expand Up @@ -226,11 +228,13 @@ reorder these checks**:

1. `?` toggles help (except when search focused)
2. Any key closes help when showing
3. `focusDetailSearch` — consumes all keys except Esc/Enter
4. `focusSearch` — consumes all keys except Tab/Esc
5. Trace tab j/k — when detail panel open and Trace tab active
6. General j/k, G/g, Enter/Esc, Tab, etc.
7. Single-key shortcuts: `w`, `f`, `/`, `s`, `t`, `q`
3. SQL preview — D/Esc close, Ctrl+D/U scroll
4. `Ctrl+K` — opens spotlight (quick navigation)
5. `focusDetailSearch` — consumes all keys except Esc/Enter
6. `focusSearch` — consumes all keys except Tab/Esc
7. Trace tab j/k — when detail panel open and Trace tab active
8. General j/k, G/g, Enter/Esc, Tab, etc.
9. Single-key shortcuts: `w`, `f`, `/`, `s`, `t`, `q`

### Dynamic Table Columns

Expand Down
17 changes: 10 additions & 7 deletions packages/cli/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ src/
│ ├── RowOverview.tsx # Structured overview (Top Level, Attributes, Resources)
│ ├── ColumnValues.tsx # Shared key-value renderer with scroll support
│ ├── LoginForm.tsx # Email/password login form (used inside TUI App)
│ └── SourcePicker.tsx # j/k source selector
│ ├── SourcePicker.tsx # j/k source selector
│ └── Spotlight.tsx # Ctrl+K spotlight overlay for quick navigation
├── shared/ # Logic ported from packages/app (@source annotated)
│ ├── useRowWhere.ts # processRowToWhereClause, buildColumnMap, getRowWhere
│ ├── source.ts # getDisplayedTimestampValueExpression, getEventBody, etc.
Expand Down Expand Up @@ -181,12 +182,14 @@ reorder these checks**:

1. `?` toggles help (except when search focused)
2. Any key closes help when showing
3. `focusDetailSearch` — consumes all keys except Esc/Enter
4. `focusSearch` — consumes all keys except Tab/Esc
5. Trace tab j/k + Ctrl+D/U — when detail panel open and Trace tab active
6. Column Values / Overview Ctrl+D/U — scroll detail view
7. General j/k, G/g, Enter/Esc, Tab, etc.
8. Single-key shortcuts: `w`, `f`, `/`, `s`, `t`, `q`
3. SQL preview — D/Esc close, Ctrl+D/U scroll
4. `Ctrl+K` — opens spotlight (quick navigation)
5. `focusDetailSearch` — consumes all keys except Esc/Enter
6. `focusSearch` — consumes all keys except Tab/Esc
7. Trace tab j/k + Ctrl+D/U — when detail panel open and Trace tab active
8. Column Values / Overview Ctrl+D/U — scroll detail view
9. General j/k, G/g, Enter/Esc, Tab, etc.
10. Single-key shortcuts: `w`, `f`, `/`, `s`, `t`, `q`

### Follow Mode

Expand Down
158 changes: 116 additions & 42 deletions packages/cli/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Box, Text } from 'ink';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { Box, Text, useStdout } from 'ink';
import Spinner from 'ink-spinner';

import { SourceKind } from '@hyperdx/common-utils/dist/types';
Expand All @@ -13,6 +13,10 @@ import AlertsPage from '@/components/AlertsPage';
import ErrorDisplay from '@/components/ErrorDisplay';
import LoginForm from '@/components/LoginForm';
import SourcePicker from '@/components/SourcePicker';
import Spotlight, {
buildSpotlightItems,
type SpotlightItem,
} from '@/components/Spotlight';
import EventViewer from '@/components/EventViewer';

type Screen = 'loading' | 'login' | 'pick-source' | 'events' | 'alerts';
Expand All @@ -28,6 +32,9 @@ interface AppProps {
}

export default function App({ apiUrl, query, sourceName, follow }: AppProps) {
const { stdout } = useStdout();
const termHeight = stdout?.rows ?? 24;

const [screen, setScreen] = useState<Screen>('loading');
const [client] = useState(() => new ApiClient({ apiUrl }));
const [eventSources, setLogSources] = useState<SourceResponse[]>([]);
Expand All @@ -37,6 +44,7 @@ export default function App({ apiUrl, query, sourceName, follow }: AppProps) {
);
const [activeQuery, setActiveQuery] = useState(query ?? '');
const [error, setError] = useState<string | null>(null);
const [showSpotlight, setShowSpotlight] = useState(false);

// Check existing session on mount
useEffect(() => {
Expand Down Expand Up @@ -135,6 +143,54 @@ export default function App({ apiUrl, query, sourceName, follow }: AppProps) {
setScreen(preAlertsScreen);
}, [preAlertsScreen]);

// ---- Spotlight (Ctrl+K) --------------------------------------------

const spotlightItems = useMemo(
() => buildSpotlightItems(eventSources, savedSearches),
[eventSources, savedSearches],
);

const handleOpenSpotlight = useCallback(() => {
setShowSpotlight(true);
}, []);

const handleCloseSpotlight = useCallback(() => {
setShowSpotlight(false);
}, []);

const handleSpotlightSelect = useCallback(
(item: SpotlightItem) => {
setShowSpotlight(false);
switch (item.type) {
case 'source':
if (item.source) {
setSelectedSource(item.source);
setActiveQuery('');
setScreen('events');
}
break;
case 'saved-search':
if (item.search) {
const source = eventSources.find(
s => s.id === item.search!.source || s._id === item.search!.source,
);
if (source) {
setSelectedSource(source);
}
setActiveQuery(item.search.where);
setScreen('events');
}
break;
case 'page':
if (item.page === 'alerts') {
handleOpenAlerts();
}
break;
}
},
[eventSources, handleOpenAlerts],
);

if (error) {
return (
<Box paddingX={1}>
Expand All @@ -143,53 +199,71 @@ export default function App({ apiUrl, query, sourceName, follow }: AppProps) {
);
}

switch (screen) {
case 'loading':
return (
<Box paddingX={1}>
<Text>
<Spinner type="dots" /> Connecting to {apiUrl}…
</Text>
</Box>
);
const renderScreen = () => {
switch (screen) {
case 'loading':
return (
<Box paddingX={1}>
<Text>
<Spinner type="dots" /> Connecting to {apiUrl}…
</Text>
</Box>
);

case 'login':
return <LoginForm apiUrl={apiUrl} onLogin={handleLogin} />;
case 'login':
return <LoginForm apiUrl={apiUrl} onLogin={handleLogin} />;

case 'pick-source':
return (
<Box flexDirection="column">
<Box flexDirection="column" marginBottom={1}>
<Text color="#00c28a" bold>
HyperDX TUI
</Text>
<Text dimColor>Search and tail events from the terminal</Text>
case 'pick-source':
return (
<Box flexDirection="column">
<Box flexDirection="column" marginBottom={1}>
<Text color="#00c28a" bold>
HyperDX TUI
</Text>
<Text dimColor>Search and tail events from the terminal</Text>
</Box>
<SourcePicker
sources={eventSources}
onSelect={handleSourceSelect}
onOpenAlerts={handleOpenAlerts}
onOpenSpotlight={handleOpenSpotlight}
/>
</Box>
<SourcePicker
);

case 'alerts':
return <AlertsPage client={client} onClose={handleCloseAlerts} />;

case 'events':
if (!selectedSource) return null;
return (
<EventViewer
clickhouseClient={client.createClickHouseClient()}
metadata={client.createMetadata()}
source={selectedSource}
sources={eventSources}
onSelect={handleSourceSelect}
savedSearches={savedSearches}
onSavedSearchSelect={handleSavedSearchSelect}
onOpenAlerts={handleOpenAlerts}
onOpenSpotlight={handleOpenSpotlight}
initialQuery={activeQuery}
follow={follow}
/>
</Box>
);
);
}
};

case 'alerts':
return <AlertsPage client={client} onClose={handleCloseAlerts} />;

case 'events':
if (!selectedSource) return null;
return (
<EventViewer
clickhouseClient={client.createClickHouseClient()}
metadata={client.createMetadata()}
source={selectedSource}
sources={eventSources}
savedSearches={savedSearches}
onSavedSearchSelect={handleSavedSearchSelect}
onOpenAlerts={handleOpenAlerts}
initialQuery={activeQuery}
follow={follow}
if (showSpotlight) {
return (
<Box flexDirection="column" height={termHeight}>
<Spotlight
items={spotlightItems}
onSelect={handleSpotlightSelect}
onClose={handleCloseSpotlight}
/>
);
</Box>
);
}

return renderScreen();
}
2 changes: 2 additions & 0 deletions packages/cli/src/components/EventViewer/EventViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export default function EventViewer({
savedSearches,
onSavedSearchSelect,
onOpenAlerts,
onOpenSpotlight,
initialQuery = '',
follow = true,
}: EventViewerProps) {
Expand Down Expand Up @@ -182,6 +183,7 @@ export default function EventViewer({
findActiveIndex,
onSavedSearchSelect,
onOpenAlerts,
onOpenSpotlight,
setFocusSearch,
setFocusDetailSearch,
setShowHelp,
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/components/EventViewer/SubComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export const HelpScreen = React.memo(function HelpScreen() {
['D', 'Show generated SQL'],
['f', 'Toggle follow mode (live tail)'],
['w', 'Toggle line wrap'],
['Ctrl+K', 'Open spotlight (quick navigation)'],
['A (Shift+A)', 'Open alerts page'],
['?', 'Toggle this help'],
['q', 'Quit'],
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/components/EventViewer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface EventViewerProps {
savedSearches: SavedSearchResponse[];
onSavedSearchSelect: (search: SavedSearchResponse) => void;
onOpenAlerts?: () => void;
onOpenSpotlight?: () => void;
initialQuery?: string;
follow?: boolean;
}
Expand Down
8 changes: 8 additions & 0 deletions packages/cli/src/components/EventViewer/useKeybindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export interface KeybindingParams {

// Navigation
onOpenAlerts?: () => void;
onOpenSpotlight?: () => void;

// State setters
setFocusSearch: React.Dispatch<React.SetStateAction<boolean>>;
Expand Down Expand Up @@ -98,6 +99,7 @@ export function useKeybindings(params: KeybindingParams): void {
findActiveIndex,
onSavedSearchSelect,
onOpenAlerts,
onOpenSpotlight,
setFocusSearch,
setFocusDetailSearch,
setShowHelp,
Expand Down Expand Up @@ -175,6 +177,12 @@ export function useKeybindings(params: KeybindingParams): void {
return;
}

// Ctrl+K opens spotlight from anywhere (except text inputs)
if (key.ctrl && input === 'k' && onOpenSpotlight) {
onOpenSpotlight();
return;
}

if (focusDetailSearch) {
if (key.escape || key.return) {
setFocusDetailSearch(false);
Expand Down
10 changes: 9 additions & 1 deletion packages/cli/src/components/SourcePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,22 @@ interface SourcePickerProps {
sources: SourceResponse[];
onSelect: (source: SourceResponse) => void;
onOpenAlerts?: () => void;
onOpenSpotlight?: () => void;
}

export default function SourcePicker({
sources,
onSelect,
onOpenAlerts,
onOpenSpotlight,
}: SourcePickerProps) {
const [selected, setSelected] = useState(0);

useInput((input, key) => {
if (key.ctrl && input === 'k' && onOpenSpotlight) {
onOpenSpotlight();
return;
}
if (input === 'A' && onOpenAlerts) {
onOpenAlerts();
return;
Expand Down Expand Up @@ -48,7 +54,9 @@ export default function SourcePicker({
</Text>
))}
<Text> </Text>
<Text dimColor>j/k to navigate, Enter/l to select, A=alerts</Text>
<Text dimColor>
j/k to navigate, Enter/l to select, Ctrl+K=spotlight, A=alerts
</Text>
</Box>
);
}
Loading
Loading