diff --git a/src/Web/autopilot-monitor-web/app/sessions/components/EventTimeline.tsx b/src/Web/autopilot-monitor-web/app/sessions/components/EventTimeline.tsx
index a3feda3d..d2a2d87c 100644
--- a/src/Web/autopilot-monitor-web/app/sessions/components/EventTimeline.tsx
+++ b/src/Web/autopilot-monitor-web/app/sessions/components/EventTimeline.tsx
@@ -3,10 +3,16 @@
import { useState, useMemo } from "react";
import { EnrollmentEvent, Session } from "@/types";
import { normalizeEventDataForDisplay, shortenBuildHashInMessage } from "../utils/eventHelpers";
+import { buildEventSearchMatcher, parseEventSearchQuery } from "../utils/eventSearchQuery";
import { getEnrichedOrLookup, formatErrorCode, type ErrorCodeEntry } from "@/utils/errorCodeMap";
import { readTimeProvenance, classifyTimeJump, readClockChangeDeltaMs } from "@/lib/timeProvenance";
import { formatDuration, formatUtcOffset } from "@/lib/formatting";
+const SEARCH_SYNTAX_HINT =
+ "Searches event type, message and source. Several terms are combined with AND. " +
+ "A leading minus hides matches, e.g. -app_install_progress. " +
+ 'Quote a term to take it literally, e.g. "-1".';
+
interface EventTimelineProps {
filteredEvents: EnrollmentEvent[];
events: EnrollmentEvent[];
@@ -60,14 +66,16 @@ export default function EventTimeline({
const [searchQuery, setSearchQuery] = useState("");
const [rawMode, setRawMode] = useState(false);
- const matchesSearch = useMemo(() => {
- if (!searchQuery.trim()) return null;
- const q = searchQuery.toLowerCase();
- return (event: EnrollmentEvent) =>
- event.eventType?.toLowerCase().includes(q) ||
- event.message?.toLowerCase().includes(q) ||
- event.source?.toLowerCase().includes(q);
- }, [searchQuery]);
+ // Terms are AND-ed, a leading minus excludes — see utils/eventSearchQuery.ts.
+ const matchesSearch = useMemo(() => buildEventSearchMatcher(searchQuery), [searchQuery]);
+ const excludedTerms = useMemo(() => parseEventSearchQuery(searchQuery).exclude, [searchQuery]);
+
+ // The counter has to follow the search, otherwise excluding event types leaves it
+ // reporting a number of rows the timeline no longer shows.
+ const searchVisibleCount = useMemo(
+ () => (matchesSearch ? filteredEvents.filter(matchesSearch).length : filteredEvents.length),
+ [filteredEvents, matchesSearch],
+ );
const sortedBySequence = useMemo(() => {
let filtered = events.filter(e => severityFilters.has(e.severity));
@@ -102,7 +110,8 @@ export default function EventTimeline({
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
- placeholder="Search events..."
+ placeholder="Search or -exclude..."
+ title={SEARCH_SYNTAX_HINT}
className="w-full pl-7 pr-7 py-1 text-xs border border-gray-300 rounded-full focus:outline-none focus:ring-1 focus:ring-green-500 focus:border-green-500"
/>
{searchQuery && (
@@ -139,7 +148,15 @@ export default function EventTimeline({
);
})}
- ({filteredEvents.length}/{events.length})
+ ({searchVisibleCount}/{events.length})
+ {excludedTerms.length > 0 && (
+ `"${t}"`).join(", ")} are hidden.`}
+ >
+ hiding {excludedTerms.join(", ")}
+
+ )}