feat(app): search history by workspace, agent, and branch (#2995)

History was a date-grouped list you scrolled. Finding a session from last
week meant eyeballing every row. This adds a search field to the existing
filter rail, next to the host picker.

Search runs on the daemon, not the client. The daemon already holds every
persisted agent in memory, so it ranks the whole history and the client
never has to warn that it only looked at the pages it had loaded.

A searched response is one ranked page with no cursor. The global top K is
always contained in the union of each host's own top K, so merging per-host
pages and slicing back to the limit is the exact global answer — no k-way
merge, and no offset into a ranking that is recomputed per request, which
would duplicate and skip rows as history changes underneath it. Past result
200 of a relevance order the answer is a narrower query, so the footer says
that instead of offering a "Load more" that cannot deliver.

Matching is the app's existing picker matcher, moved to the protocol package
so app and daemon share one implementation instead of drifting into two
"why didn't this show up?" rule sets. Typo tolerance is opt-in, so the
comboboxes keep exact narrowing; the budget is per token, and four-character
tokens get transpositions only, because at that length a free substitution
turns "main" into "mail", "maid" and "rain".

Ranked rows carry the spans they matched on, so results can mark them. The
subsequence and typo tiers match characters the eye would never find
unaided, and an unexplained row reads as a bug. The ranker computes the
spans anyway; sending them keeps the client from deriving a second opinion
that could disagree with the ranking it is explaining.

Hosts that fail or are offline are now named above the list. Without that,
"No sessions match" is a claim the app has no basis for.

Gated on server_info.features.agentHistorySearch, checked once against the
connected hosts. Reachability is reported by the banner instead, so one
offline host in the registry no longer hides the field.
This commit is contained in:
Mohamed Boudra
2026-08-08 10:24:51 +02:00
committed by GitHub
parent cc5ce1038d
commit b167ee02e1
38 changed files with 2549 additions and 253 deletions
+5
View File
@@ -231,8 +231,13 @@ export const ar: TranslationResources = {
sessions: {
title: "السجل",
empty: "لا توجد جلسات بعد",
noMatches: "لا توجد جلسات مطابقة",
tooManyMatches: "نتائج كثيرة جدًا — ضيّق نطاق البحث",
hostLoadFailed: "{{host}}: تعذر تحميل السجل",
searchPlaceholder: "البحث في السجل",
actions: {
loadMore: "تحميل المزيد",
clearSearch: "مسح البحث",
},
},
agentList: {
+5
View File
@@ -230,8 +230,13 @@ export const en = {
sessions: {
title: "History",
empty: "No sessions yet",
noMatches: "No sessions match",
tooManyMatches: "Too many matches — narrow your search",
hostLoadFailed: "{{host}}: Could not load history",
searchPlaceholder: "Search history",
actions: {
loadMore: "Load more",
clearSearch: "Clear search",
},
},
agentList: {
+5
View File
@@ -234,8 +234,13 @@ export const es: TranslationResources = {
sessions: {
title: "Historial",
empty: "Aún no hay sesiones",
noMatches: "No hay sesiones que coincidan",
tooManyMatches: "Demasiadas coincidencias: acota la búsqueda",
hostLoadFailed: "{{host}}: No se pudo cargar el historial",
searchPlaceholder: "Buscar en el historial",
actions: {
loadMore: "Cargar más",
clearSearch: "Borrar búsqueda",
},
},
agentList: {
+5
View File
@@ -235,8 +235,13 @@ export const fr: TranslationResources = {
sessions: {
title: "Historique",
empty: "Aucune séance pour l'instant",
noMatches: "Aucune séance correspondante",
tooManyMatches: "Trop de résultats — affinez la recherche",
hostLoadFailed: "{{host}} : Impossible de charger l'historique",
searchPlaceholder: "Rechercher dans l'historique",
actions: {
loadMore: "Charger plus",
clearSearch: "Effacer la recherche",
},
},
agentList: {
+5
View File
@@ -234,8 +234,13 @@ export const ja: TranslationResources = {
sessions: {
title: "履歴",
empty: "セッションがまだありません",
noMatches: "一致するセッションはありません",
tooManyMatches: "一致が多すぎます — 検索条件を絞ってください",
hostLoadFailed: "{{host}}: 履歴を読み込めませんでした",
searchPlaceholder: "履歴を検索",
actions: {
loadMore: "さらに読み込む",
clearSearch: "検索をクリア",
},
},
agentList: {
+5
View File
@@ -232,8 +232,13 @@ export const ko: TranslationResources = {
sessions: {
title: "기록",
empty: "아직 세션이 없습니다",
noMatches: "일치하는 세션이 없습니다",
tooManyMatches: "일치 항목이 너무 많습니다 — 검색 범위를 좁히세요",
hostLoadFailed: "{{host}}: 기록을 불러오지 못했습니다",
searchPlaceholder: "기록 검색",
actions: {
loadMore: "더 불러오기",
clearSearch: "검색 지우기",
},
},
agentList: {
+5
View File
@@ -234,8 +234,13 @@ export const ptBR: TranslationResources = {
sessions: {
title: "Histórico de agentes",
empty: "Nenhuma sessão ainda",
noMatches: "Nenhuma sessão corresponde",
tooManyMatches: "Muitos resultados — refine a busca",
hostLoadFailed: "{{host}}: Não foi possível carregar o histórico",
searchPlaceholder: "Buscar no histórico",
actions: {
loadMore: "Carregar mais",
clearSearch: "Limpar busca",
},
},
agentList: {
+5
View File
@@ -233,8 +233,13 @@ export const ru: TranslationResources = {
sessions: {
title: "История",
empty: "Сеансов пока нет",
noMatches: "Нет подходящих сеансов",
tooManyMatches: "Слишком много совпадений — уточните запрос",
hostLoadFailed: "{{host}}: Не удалось загрузить историю",
searchPlaceholder: "Поиск по истории",
actions: {
loadMore: "Загрузить больше",
clearSearch: "Очистить поиск",
},
},
agentList: {
+5
View File
@@ -231,8 +231,13 @@ export const zhCN: TranslationResources = {
sessions: {
title: "历史",
empty: "还没有会话",
noMatches: "没有匹配的会话",
tooManyMatches: "匹配过多 — 请缩小搜索范围",
hostLoadFailed: "{{host}}:无法加载历史",
searchPlaceholder: "搜索历史",
actions: {
loadMore: "加载更多",
clearSearch: "清除搜索",
},
},
agentList: {