Skip to content

Commit 5f8ae7a

Browse files
committed
feat(recall): add corpus browser page
Distilled entries need a corpus-wide inspection surface so extraction coverage and output quality can be assessed beyond one transcript. Expose read-only manager coverage and exact review-state filtering while keeping the first page bounded and management actions out of this slice.
1 parent 34d326c commit 5f8ae7a

20 files changed

Lines changed: 1141 additions & 0 deletions

File tree

cmd/agentsview/main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"go.kenn.io/agentsview/internal/config"
2424
"go.kenn.io/agentsview/internal/db"
2525
"go.kenn.io/agentsview/internal/parser"
26+
"go.kenn.io/agentsview/internal/recall/extract"
2627
"go.kenn.io/agentsview/internal/remotesync"
2728
"go.kenn.io/agentsview/internal/secrets"
2829
"go.kenn.io/agentsview/internal/server"
@@ -481,6 +482,10 @@ func runServe(cfg config.Config, opts serveOptions) {
481482
// no sync activity follows. Notify never blocks.
482483
srvOpts = append(srvOpts,
483484
server.WithSessionMutationNotifier(extractSched.Notify))
485+
if manager, ok := extractSched.mgr.(*extract.Manager); ok {
486+
srvOpts = append(srvOpts,
487+
server.WithRecallExtractionStatusProvider(manager))
488+
}
484489
}
485490
if engine != nil {
486491
srvOpts = append(srvOpts, server.WithLocalSyncRunner(

frontend/messages/en.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"nav_more_navigation": "More navigation",
2020
"nav_more": "More",
2121
"nav_trends": "Trends",
22+
"nav_recall": "Recall",
2223
"nav_pinned": "Pinned",
2324
"nav_insights": "Insights",
2425
"nav_trash": "Trash",
@@ -1151,6 +1152,30 @@
11511152
"session_recall_provenance_revoked": "Provenance revoked",
11521153
"session_recall_evidence_range": "Messages {start}–{end}",
11531154
"session_recall_jump_evidence": "Jump to transcript messages {start}–{end}",
1155+
"recall_page_title": "Recall",
1156+
"recall_page_subtitle": "Browse distilled entries and extraction coverage.",
1157+
"recall_page_search_placeholder": "Search Recall entries…",
1158+
"recall_page_clear_search": "Clear Recall search",
1159+
"recall_page_type_filter": "Entry type",
1160+
"recall_page_all_types": "All types",
1161+
"recall_page_generation_filter": "Extraction generation",
1162+
"recall_page_all_generations": "All generations",
1163+
"recall_page_review_filter": "Review state",
1164+
"recall_page_all_review_states": "All review states",
1165+
"recall_page_loading": "Loading Recall entries…",
1166+
"recall_page_empty": "No Recall entries match these filters.",
1167+
"recall_page_error": "Could not load Recall entries.",
1168+
"recall_page_entries_shown": "Entries shown: {countLabel}",
1169+
"recall_page_extraction_title": "Extraction status",
1170+
"recall_page_extraction_unconfigured": "Recall extraction is not configured.",
1171+
"recall_page_extraction_error": "Could not load extraction status.",
1172+
"recall_page_status_done": "{countLabel} done",
1173+
"recall_page_status_failed": "{countLabel} failed",
1174+
"recall_page_status_eligible": "{countLabel} eligible",
1175+
"recall_page_status_pending": "{countLabel} pending",
1176+
"recall_page_status_partial": "{countLabel} partial",
1177+
"recall_page_status_units": "Units {doneLabel}/{totalLabel}",
1178+
"recall_page_status_entries": "{countLabel} entries",
11541179
"session_vitals_title": "Analysis",
11551180
"session_vitals_subtitle": "Session vital signs",
11561181
"session_vitals_close": "Close session analysis",

frontend/messages/fr.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"nav_more_navigation": "Plus de navigation",
2020
"nav_more": "Plus",
2121
"nav_trends": "Tendances",
22+
"nav_recall": "Rappel",
2223
"nav_pinned": "Épinglés",
2324
"nav_insights": "Analyses",
2425
"nav_trash": "Corbeille",
@@ -1151,6 +1152,30 @@
11511152
"session_recall_provenance_revoked": "Provenance révoquée",
11521153
"session_recall_evidence_range": "Messages {start}–{end}",
11531154
"session_recall_jump_evidence": "Aller aux messages {start}–{end} de la transcription",
1155+
"recall_page_title": "Rappel",
1156+
"recall_page_subtitle": "Parcourir les entrées distillées et la couverture d’extraction.",
1157+
"recall_page_search_placeholder": "Rechercher dans le rappel…",
1158+
"recall_page_clear_search": "Effacer la recherche de rappel",
1159+
"recall_page_type_filter": "Type d’entrée",
1160+
"recall_page_all_types": "Tous les types",
1161+
"recall_page_generation_filter": "Génération d’extraction",
1162+
"recall_page_all_generations": "Toutes les générations",
1163+
"recall_page_review_filter": "État de révision",
1164+
"recall_page_all_review_states": "Tous les états de révision",
1165+
"recall_page_loading": "Chargement des entrées de rappel…",
1166+
"recall_page_empty": "Aucune entrée de rappel ne correspond à ces filtres.",
1167+
"recall_page_error": "Impossible de charger les entrées de rappel.",
1168+
"recall_page_entries_shown": "Entrées affichées : {countLabel}",
1169+
"recall_page_extraction_title": "État de l’extraction",
1170+
"recall_page_extraction_unconfigured": "L’extraction du rappel n’est pas configurée.",
1171+
"recall_page_extraction_error": "Impossible de charger l’état de l’extraction.",
1172+
"recall_page_status_done": "{countLabel} terminées",
1173+
"recall_page_status_failed": "{countLabel} échouées",
1174+
"recall_page_status_eligible": "{countLabel} éligibles",
1175+
"recall_page_status_pending": "{countLabel} en attente",
1176+
"recall_page_status_partial": "{countLabel} partielles",
1177+
"recall_page_status_units": "Unités {doneLabel}/{totalLabel}",
1178+
"recall_page_status_entries": "{countLabel} entrées",
11541179
"session_vitals_title": "Analyse",
11551180
"session_vitals_subtitle": "Signes vitaux de la session",
11561181
"session_vitals_close": "Fermer l'analyse de la session",

frontend/messages/ko.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"nav_more_navigation": "추가 내비게이션",
2020
"nav_more": "더 보기",
2121
"nav_trends": "트렌드",
22+
"nav_recall": "리콜",
2223
"nav_pinned": "고정됨",
2324
"nav_insights": "인사이트",
2425
"nav_trash": "휴지통",
@@ -1121,6 +1122,30 @@
11211122
"session_recall_provenance_revoked": "출처가 취소됨",
11221123
"session_recall_evidence_range": "메시지 {start}–{end}",
11231124
"session_recall_jump_evidence": "대화 기록 메시지 {start}–{end}(으)로 이동",
1125+
"recall_page_title": "리콜",
1126+
"recall_page_subtitle": "추출된 항목과 추출 범위를 살펴봅니다.",
1127+
"recall_page_search_placeholder": "리콜 항목 검색…",
1128+
"recall_page_clear_search": "리콜 검색 지우기",
1129+
"recall_page_type_filter": "항목 유형",
1130+
"recall_page_all_types": "모든 유형",
1131+
"recall_page_generation_filter": "추출 세대",
1132+
"recall_page_all_generations": "모든 세대",
1133+
"recall_page_review_filter": "검토 상태",
1134+
"recall_page_all_review_states": "모든 검토 상태",
1135+
"recall_page_loading": "리콜 항목을 불러오는 중…",
1136+
"recall_page_empty": "이 필터와 일치하는 리콜 항목이 없습니다.",
1137+
"recall_page_error": "리콜 항목을 불러올 수 없습니다.",
1138+
"recall_page_entries_shown": "표시된 항목: {countLabel}",
1139+
"recall_page_extraction_title": "추출 상태",
1140+
"recall_page_extraction_unconfigured": "리콜 추출이 구성되지 않았습니다.",
1141+
"recall_page_extraction_error": "추출 상태를 불러올 수 없습니다.",
1142+
"recall_page_status_done": "완료 {countLabel}",
1143+
"recall_page_status_failed": "실패 {countLabel}",
1144+
"recall_page_status_eligible": "대상 {countLabel}",
1145+
"recall_page_status_pending": "대기 {countLabel}",
1146+
"recall_page_status_partial": "부분 완료 {countLabel}",
1147+
"recall_page_status_units": "단위 {doneLabel}/{totalLabel}",
1148+
"recall_page_status_entries": "항목 {countLabel}",
11241149
"session_vitals_title": "분석",
11251150
"session_vitals_subtitle": "세션 바이탈 사인",
11261151
"session_vitals_close": "세션 분석 닫기",

frontend/messages/zh-CN.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"nav_more_navigation": "更多导航",
2020
"nav_more": "更多",
2121
"nav_trends": "趋势",
22+
"nav_recall": "召回",
2223
"nav_pinned": "已固定",
2324
"nav_insights": "洞察",
2425
"nav_trash": "回收站",
@@ -1119,6 +1120,30 @@
11191120
"session_recall_provenance_revoked": "来源已撤销",
11201121
"session_recall_evidence_range": "消息 {start}–{end}",
11211122
"session_recall_jump_evidence": "跳转到转录消息 {start}–{end}",
1123+
"recall_page_title": "召回",
1124+
"recall_page_subtitle": "浏览提炼条目和提取覆盖情况。",
1125+
"recall_page_search_placeholder": "搜索召回条目…",
1126+
"recall_page_clear_search": "清除召回搜索",
1127+
"recall_page_type_filter": "条目类型",
1128+
"recall_page_all_types": "所有类型",
1129+
"recall_page_generation_filter": "提取代次",
1130+
"recall_page_all_generations": "所有代次",
1131+
"recall_page_review_filter": "审核状态",
1132+
"recall_page_all_review_states": "所有审核状态",
1133+
"recall_page_loading": "正在加载召回条目…",
1134+
"recall_page_empty": "没有符合这些筛选条件的召回条目。",
1135+
"recall_page_error": "无法加载召回条目。",
1136+
"recall_page_entries_shown": "显示条目:{countLabel}",
1137+
"recall_page_extraction_title": "提取状态",
1138+
"recall_page_extraction_unconfigured": "尚未配置召回提取。",
1139+
"recall_page_extraction_error": "无法加载提取状态。",
1140+
"recall_page_status_done": "已完成 {countLabel}",
1141+
"recall_page_status_failed": "失败 {countLabel}",
1142+
"recall_page_status_eligible": "符合条件 {countLabel}",
1143+
"recall_page_status_pending": "待处理 {countLabel}",
1144+
"recall_page_status_partial": "部分完成 {countLabel}",
1145+
"recall_page_status_units": "单元 {doneLabel}/{totalLabel}",
1146+
"recall_page_status_entries": "条目 {countLabel}",
11221147
"session_vitals_title": "分析",
11231148
"session_vitals_subtitle": "会话指标",
11241149
"session_vitals_close": "关闭会话分析",

frontend/messages/zh-TW.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"nav_more_navigation": "更多導航",
2020
"nav_more": "更多",
2121
"nav_trends": "趨勢",
22+
"nav_recall": "召回",
2223
"nav_pinned": "已固定",
2324
"nav_insights": "洞察",
2425
"nav_trash": "回收站",
@@ -1119,6 +1120,30 @@
11191120
"session_recall_provenance_revoked": "來源已撤銷",
11201121
"session_recall_evidence_range": "訊息 {start}–{end}",
11211122
"session_recall_jump_evidence": "跳至逐字稿訊息 {start}–{end}",
1123+
"recall_page_title": "召回",
1124+
"recall_page_subtitle": "瀏覽提煉項目和擷取涵蓋情況。",
1125+
"recall_page_search_placeholder": "搜尋召回項目…",
1126+
"recall_page_clear_search": "清除召回搜尋",
1127+
"recall_page_type_filter": "項目類型",
1128+
"recall_page_all_types": "所有類型",
1129+
"recall_page_generation_filter": "擷取代次",
1130+
"recall_page_all_generations": "所有代次",
1131+
"recall_page_review_filter": "審核狀態",
1132+
"recall_page_all_review_states": "所有審核狀態",
1133+
"recall_page_loading": "正在載入召回項目…",
1134+
"recall_page_empty": "沒有符合這些篩選條件的召回項目。",
1135+
"recall_page_error": "無法載入召回項目。",
1136+
"recall_page_entries_shown": "顯示項目:{countLabel}",
1137+
"recall_page_extraction_title": "擷取狀態",
1138+
"recall_page_extraction_unconfigured": "尚未設定召回擷取。",
1139+
"recall_page_extraction_error": "無法載入擷取狀態。",
1140+
"recall_page_status_done": "已完成 {countLabel}",
1141+
"recall_page_status_failed": "失敗 {countLabel}",
1142+
"recall_page_status_eligible": "符合條件 {countLabel}",
1143+
"recall_page_status_pending": "待處理 {countLabel}",
1144+
"recall_page_status_partial": "部分完成 {countLabel}",
1145+
"recall_page_status_units": "單元 {doneLabel}/{totalLabel}",
1146+
"recall_page_status_entries": "項目 {countLabel}",
11221147
"session_vitals_title": "分析",
11231148
"session_vitals_subtitle": "會話指標",
11241149
"session_vitals_close": "關閉會話分析",

frontend/src/App.svelte

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
import UsagePage from "./lib/components/usage/UsagePage.svelte";
6161
import ActivityPage from "./lib/components/activity/ActivityPage.svelte";
6262
import TrendsPage from "./lib/components/trends/TrendsPage.svelte";
63+
import RecallPage from "./lib/components/recall/RecallPage.svelte";
6364
import InsightsPage from "./lib/components/insights/InsightsPage.svelte";
6465
import PinnedPage from "./lib/components/pinned/PinnedPage.svelte";
6566
import TrashPage from "./lib/components/trash/TrashPage.svelte";
@@ -666,6 +667,10 @@
666667
<div class="page-scroll">
667668
<TrendsPage />
668669
</div>
670+
{:else if router.route === "recall"}
671+
<div class="page-scroll">
672+
<RecallPage />
673+
</div>
669674
{:else if router.route === "insights"}
670675
<div class="page-scroll">
671676
<InsightsPage />
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {
2+
afterEach,
3+
describe,
4+
expect,
5+
it,
6+
vi,
7+
} from "vite-plus/test";
8+
import { fetchRecallEntries } from "./recall.js";
9+
10+
afterEach(() => {
11+
vi.unstubAllGlobals();
12+
});
13+
14+
describe("fetchRecallEntries", () => {
15+
it("sends every corpus-browser filter to the Recall API", async () => {
16+
const fetchMock = vi.fn().mockResolvedValue(new Response(
17+
JSON.stringify({ entries: [], trusted_only: false }),
18+
{ status: 200, headers: { "Content-Type": "application/json" } },
19+
));
20+
vi.stubGlobal("fetch", fetchMock);
21+
22+
await fetchRecallEntries({
23+
query: "bounded pass",
24+
project: "project-a",
25+
type: "decision",
26+
sourceRunId: "generation-a",
27+
reviewState: "human_reviewed",
28+
limit: 75,
29+
});
30+
31+
expect(fetchMock).toHaveBeenCalledOnce();
32+
const url = new URL(String(fetchMock.mock.calls[0]?.[0]),
33+
window.location.origin);
34+
expect(url.pathname).toBe("/api/v1/recall/entries");
35+
expect(Object.fromEntries(url.searchParams)).toEqual({
36+
limit: "75",
37+
q: "bounded pass",
38+
project: "project-a",
39+
type: "decision",
40+
source_run_id: "generation-a",
41+
review_state: "human_reviewed",
42+
});
43+
});
44+
});

frontend/src/lib/api/recall.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type {
22
RecallEntriesResponse,
33
RecallEntry,
4+
RecallEntryFilters,
5+
RecallExtractionStatus,
46
} from "./types/recall.js";
57
import {
68
ApiError,
@@ -10,6 +12,53 @@ import {
1012
} from "./runtime.js";
1113

1214
const SESSION_RECALL_LIMIT = 500;
15+
const RECALL_PAGE_LIMIT = 200;
16+
17+
export async function fetchRecallEntries(
18+
filters: RecallEntryFilters = {},
19+
signal?: AbortSignal,
20+
): Promise<RecallEntry[]> {
21+
const query = new URLSearchParams({
22+
limit: String(filters.limit ?? RECALL_PAGE_LIMIT),
23+
});
24+
if (filters.query) query.set("q", filters.query);
25+
if (filters.project) query.set("project", filters.project);
26+
if (filters.type) query.set("type", filters.type);
27+
if (filters.sourceRunId) {
28+
query.set("source_run_id", filters.sourceRunId);
29+
}
30+
if (filters.reviewState) {
31+
query.set("review_state", filters.reviewState);
32+
}
33+
const response = await fetch(
34+
`${getBase()}/recall/entries?${query.toString()}`,
35+
authHeaders({ signal }),
36+
);
37+
if (!response.ok) {
38+
throw new ApiError(
39+
response.status,
40+
await responseErrorMessage(response),
41+
);
42+
}
43+
const data = (await response.json()) as RecallEntriesResponse;
44+
return data.entries ?? [];
45+
}
46+
47+
export async function fetchRecallExtractionStatus(
48+
signal?: AbortSignal,
49+
): Promise<RecallExtractionStatus> {
50+
const response = await fetch(
51+
`${getBase()}/recall/extraction/status`,
52+
authHeaders({ signal }),
53+
);
54+
if (!response.ok) {
55+
throw new ApiError(
56+
response.status,
57+
await responseErrorMessage(response),
58+
);
59+
}
60+
return (await response.json()) as RecallExtractionStatus;
61+
}
1362

1463
export async function fetchSessionRecall(
1564
sessionId: string,

frontend/src/lib/api/types/recall.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ export interface RecallEntry {
1515
review_state: string;
1616
title: string;
1717
body: string;
18+
trigger?: string;
19+
confidence?: number;
20+
uncertainty?: string;
21+
project?: string;
22+
cwd?: string;
23+
git_branch?: string;
24+
agent?: string;
1825
source_session_id: string;
1926
source_run_id?: string;
2027
extractor_method?: string;
@@ -30,3 +37,39 @@ export interface RecallEntriesResponse {
3037
entries: RecallEntry[];
3138
trusted_only: boolean;
3239
}
40+
41+
export interface RecallExtractGeneration {
42+
fingerprint: string;
43+
state: string;
44+
model: string;
45+
segmenter: string;
46+
created_at: string;
47+
updated_at: string;
48+
}
49+
50+
export interface RecallExtractProgressStats {
51+
pending: number;
52+
partial: number;
53+
done: number;
54+
failed: number;
55+
units_done: number;
56+
units_total: number;
57+
entries: number;
58+
}
59+
60+
export interface RecallExtractionStatus {
61+
configured: boolean;
62+
fingerprint?: string;
63+
generations?: RecallExtractGeneration[];
64+
stats?: RecallExtractProgressStats;
65+
eligible_backlog?: number;
66+
}
67+
68+
export interface RecallEntryFilters {
69+
query?: string;
70+
project?: string;
71+
type?: string;
72+
sourceRunId?: string;
73+
reviewState?: string;
74+
limit?: number;
75+
}

0 commit comments

Comments
 (0)