>((counts, entry) => {
@@ -138,7 +142,6 @@ export function DebugApp() {
useEffect(() => {
initialOperation.current = "";
setOperations([]);
- setRequests([]);
setMessages([]);
setPages([]);
setOperationId("");
@@ -158,26 +161,12 @@ export function DebugApp() {
recordingRequest({ ...base, action: "console" }),
recordingRequest({ ...base, action: "pages" }),
]);
- const entries: DebugRequest[] = [];
- let since = 0;
- for (let page = 0; page < 20; page++) {
- if (cancelled) return;
- const batch = await recordingRequest({ ...base, action: "requests", since, limit: 100 });
- entries.push(...(batch.requests ?? []));
- since = batch.next_since ?? since;
- if ((batch.requests?.length ?? 0) < 100) break;
- }
if (cancelled) return;
setOperations((actions.operations ?? []).sort((a, b) => a.started_at - b.started_at));
if (initialOperation.current !== runId && actions.operations?.length) {
initialOperation.current = runId;
setOperationId(actions.operations.at(-1)!.id);
}
- setRequests(
- [...new Map(entries.map((item) => [item.id, item])).values()].sort(
- (a, b) => a.started_at - b.started_at,
- ),
- );
setMessages(consoleResult.console ?? []);
setPages(pageResult.pages ?? []);
})().catch((reason) => {
@@ -362,7 +351,7 @@ export function DebugApp() {
)}
- {(error || historyError || taskError || run?.storage_error) && (
+ {(error || requestError || historyError || taskError || run?.storage_error) && (
)}
{confirmDelete && (
diff --git a/apps/extension/src/entrypoints/debug/evidence.tsx b/apps/extension/src/entrypoints/debug/evidence.tsx
index 3435112..9cfff84 100644
--- a/apps/extension/src/entrypoints/debug/evidence.tsx
+++ b/apps/extension/src/entrypoints/debug/evidence.tsx
@@ -5,7 +5,6 @@ import {
RiArrowRightLine,
RiArrowRightUpLine,
RiCheckLine,
- RiCodeSSlashLine,
} from "@remixicon/react";
import { useEffect, useState } from "react";
import { debugRequest, recordingRequest } from "@/debug/client";
diff --git a/apps/extension/src/entrypoints/debug/use-requests.ts b/apps/extension/src/entrypoints/debug/use-requests.ts
new file mode 100644
index 0000000..a1ff269
--- /dev/null
+++ b/apps/extension/src/entrypoints/debug/use-requests.ts
@@ -0,0 +1,103 @@
+import { useEffect, useRef, useState } from "react";
+import { recordingRequest } from "@/debug/client";
+import type { DebugRequest, DebugRun } from "@/debug/types";
+
+interface RequestCache {
+ key: string;
+ since: number;
+ dropped: number;
+ entries: Map;
+ busy: boolean;
+ resync?: boolean;
+ refresh?: () => Promise;
+}
+
+/** Keep one selected run, serialize reads, and reuse successfully read pages across refreshes. */
+export function useRequests(run: DebugRun | undefined, enabled: boolean) {
+ const cache = useRef(undefined);
+ const [view, setView] = useState({ key: "", requests: [] as DebugRequest[], error: "" });
+ const session = run?.session_id ?? "";
+ const id = run?.id ?? "";
+ const key = `${session}/${id}`;
+ const pulse = run?.next_since ?? 0;
+ const dropped = run?.dropped_requests ?? 0;
+
+ useEffect(() => {
+ if (cache.current?.key !== key)
+ cache.current = { key, since: 0, dropped, entries: new Map(), busy: false };
+ const state = cache.current;
+ if (!id || !enabled) return;
+ let cancelled = false;
+ const publish = (error = "") => {
+ if (!cancelled)
+ setView({
+ key,
+ requests: [...state.entries.values()].sort(
+ (a, b) => a.started_at - b.started_at || a.sequence - b.sequence,
+ ),
+ error,
+ });
+ };
+ const reset = (removed: number) => {
+ state.entries.clear();
+ state.since = 0;
+ state.dropped = removed;
+ state.resync = false;
+ publish();
+ };
+ const update = async () => {
+ if (cancelled) return;
+ // The protocol has no deletion cursor: resync when retention removed requests.
+ if (state.resync || dropped > state.dropped) reset(Math.max(dropped, state.dropped));
+ for (let page = 0; page < 21; page++) {
+ if (cancelled) return;
+ const since = state.since;
+ const batch = await recordingRequest({
+ session_id: session,
+ run_id: id,
+ action: "requests",
+ since,
+ limit: 100,
+ });
+ if (cache.current !== state) return;
+ const removed = batch.run?.dropped_requests ?? state.dropped;
+ if (removed > state.dropped) {
+ reset(removed);
+ if (since > 0) continue;
+ }
+ // A fallback read may omit older stored rows; never advance past them permanently.
+ if (batch.run?.coverage.includes("evidence_read_failed")) state.resync = true;
+ const entries = batch.requests ?? [];
+ for (const entry of entries) state.entries.set(entry.id, entry);
+ state.since = Math.max(since, batch.next_since ?? since);
+ // Commit progress even when a newer pulse queued another read.
+ if (entries.length < 100 || state.since === since) break;
+ }
+ publish();
+ };
+ state.refresh = () =>
+ update().catch((reason) =>
+ publish(reason instanceof Error ? reason.message : String(reason)),
+ );
+ // Coalesce pulses to one pending refresh even if a browser read stalls.
+ if (!state.busy) {
+ state.busy = true;
+ void (async () => {
+ try {
+ while (state.refresh) {
+ const refresh = state.refresh;
+ state.refresh = undefined;
+ await refresh();
+ }
+ } finally {
+ state.busy = false;
+ }
+ })();
+ }
+ return () => {
+ cancelled = true;
+ };
+ }, [key, session, id, pulse, dropped, enabled]);
+
+ return view.key === key ? view : { requests: [], error: "" };
+}
diff --git a/packages/i18n/src/locales/en-US/extension.json b/packages/i18n/src/locales/en-US/extension.json
index 66ac976..cae056f 100644
--- a/packages/i18n/src/locales/en-US/extension.json
+++ b/packages/i18n/src/locales/en-US/extension.json
@@ -326,10 +326,8 @@
"timeline": "Action timeline",
"noOperations": "No recorded actions.",
"noSession": "This task has ended. Open history to review saved debugging records.",
- "chooseOperation": "Select an action to inspect its evidence.",
"duringRequests": "Requests in this window",
"console": "Console",
- "pageChanges": "Page changes",
"noRequests": "No retained requests.",
"noConsole": "No retained console messages.",
"correlation": "Grouped by time. Temporal proximity does not establish causality.",
@@ -359,10 +357,7 @@
"missingBody": "Body was not captured, was evicted, or is unavailable from the browser.",
"redacted": "Common credentials redacted",
"run": "Capture run",
- "selectRun": "Choose a capture run",
"noData": "No data yet",
- "elapsed": "{{ms}} ms",
- "repeat": "Repeated {{count}} times",
"state_running": "Running",
"state_completed": "Completed",
"state_error": "Failed",
@@ -378,7 +373,6 @@
"method_hover": "Hover",
"method_evaluate": "Evaluate script",
"coverage": "Capture coverage",
- "retry": "Retry",
"method_wheel": "Scroll",
"method_scroll_to": "Scroll to element",
"method_focus": "Focus",
@@ -515,7 +509,6 @@
"controlFailed": "Failed",
"controlCancelled": "Cancelled",
"originalRequest": "Original request",
- "appliedChanges": "Changed fields",
"captureRequired": "Start a new capture to add rules or replay requests.",
"controlDetails": "View rule definition",
"bodyIncomplete": "Original body is incomplete. Supply the complete body before sending.",
diff --git a/packages/i18n/src/locales/ko-KR/extension.json b/packages/i18n/src/locales/ko-KR/extension.json
index d6c6433..d0ac274 100644
--- a/packages/i18n/src/locales/ko-KR/extension.json
+++ b/packages/i18n/src/locales/ko-KR/extension.json
@@ -326,10 +326,8 @@
"timeline": "동작 타임라인",
"noOperations": "기록된 동작이 없습니다.",
"noSession": "작업이 종료되었습니다. 기록에서 저장된 디버깅 결과를 확인하세요.",
- "chooseOperation": "동작을 선택해 관련 기록을 확인하세요.",
"duringRequests": "이 시간대의 요청",
"console": "콘솔",
- "pageChanges": "페이지 변경",
"noRequests": "보관된 요청이 없습니다.",
"noConsole": "보관된 Console 메시지가 없습니다.",
"correlation": "시간순으로 묶었습니다. 시간적 인접성이 인과관계를 의미하지는 않습니다.",
@@ -359,10 +357,7 @@
"missingBody": "본문이 수집되지 않았거나 삭제되었거나 브라우저에서 제공하지 않습니다.",
"redacted": "일반적인 인증 정보 가림",
"run": "수집 실행",
- "selectRun": "수집 실행 선택",
"noData": "아직 데이터 없음",
- "elapsed": "{{ms}} ms",
- "repeat": "{{count}}번 반복",
"state_running": "실행 중",
"state_completed": "완료",
"state_error": "실패",
@@ -378,7 +373,6 @@
"method_hover": "마우스 올리기",
"method_evaluate": "스크립트 실행",
"coverage": "수집 범위",
- "retry": "다시 시도",
"method_wheel": "스크롤",
"method_scroll_to": "요소로 스크롤",
"method_focus": "포커스",
@@ -515,7 +509,6 @@
"controlFailed": "실패",
"controlCancelled": "취소됨",
"originalRequest": "원본 요청",
- "appliedChanges": "변경된 필드",
"captureRequired": "규칙 추가 또는 재전송을 위해 새 캡처를 시작하세요.",
"controlDetails": "규칙 정의 보기",
"bodyIncomplete": "원본 본문이 불완전합니다. 전송 전에 전체 본문을 입력하세요.",
diff --git a/packages/i18n/src/locales/zh-CN/extension.json b/packages/i18n/src/locales/zh-CN/extension.json
index 4c602ed..249f948 100644
--- a/packages/i18n/src/locales/zh-CN/extension.json
+++ b/packages/i18n/src/locales/zh-CN/extension.json
@@ -326,10 +326,8 @@
"timeline": "操作时间线",
"noOperations": "暂无已记录的操作。",
"noSession": "当前任务已结束。请从历史记录查看已保存的调试结果。",
- "chooseOperation": "选择一次操作,查看相关证据。",
"duringRequests": "期间请求",
"console": "Console",
- "pageChanges": "页面变化",
"noRequests": "暂无已保留的请求。",
"noConsole": "暂无已保留的 Console 消息。",
"correlation": "按发生时间归组;时间相邻不代表存在因果关系。",
@@ -359,10 +357,7 @@
"missingBody": "正文未采集、已淘汰或浏览器未能提供。",
"redacted": "常见凭据已脱敏",
"run": "采集轮次",
- "selectRun": "选择采集轮次",
"noData": "暂无数据",
- "elapsed": "{{ms}} ms",
- "repeat": "重复 {{count}} 次",
"state_running": "执行中",
"state_completed": "已执行",
"state_error": "执行失败",
@@ -378,7 +373,6 @@
"method_hover": "悬停",
"method_evaluate": "执行脚本",
"coverage": "采集范围与限制",
- "retry": "重试",
"method_wheel": "滚动",
"method_scroll_to": "滚动到元素",
"method_focus": "聚焦",
@@ -515,7 +509,6 @@
"controlFailed": "执行失败",
"controlCancelled": "已取消",
"originalRequest": "原请求",
- "appliedChanges": "修改项",
"captureRequired": "开始新的调试记录后,可以添加规则或重放请求。",
"controlDetails": "查看规则定义",
"bodyIncomplete": "原正文不完整;发送前请提供完整正文。",