Load current terminal session tail
This commit is contained in:
@@ -99,6 +99,21 @@ describe("ServerManagementTerminalDrawer", () => {
|
|||||||
expect(systemLine?.querySelector("time")?.textContent).toBe(formatTerminalServerTime(serverTime));
|
expect(systemLine?.querySelector("time")?.textContent).toBe(formatTerminalServerTime(serverTime));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("loads the current session tail when stream metadata arrives", async () => {
|
||||||
|
const stream = logStream("stdout-current", "session-current", "process.stdout");
|
||||||
|
stream.latestSeq = 42;
|
||||||
|
apiMocks.queryLogStream.mockResolvedValue({ logStreamId: stream.id, entries: [logEntry(41, "tail before drawer opened"), logEntry(42, "latest stored output")], nextSeq: 42, latestSeq: 42 });
|
||||||
|
await renderDrawer();
|
||||||
|
|
||||||
|
await emitSession("session-current");
|
||||||
|
await emitStream(stream);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(container?.textContent).toContain("tail before drawer opened");
|
||||||
|
expect(container?.textContent).toContain("latest stored output");
|
||||||
|
expect(apiMocks.queryLogStream).toHaveBeenCalledWith({ logStreamId: stream.id, afterSeq: 0, limit: 500 });
|
||||||
|
});
|
||||||
|
|
||||||
it("renders an empty current session without accepting unrelated or sessionless logs", async () => {
|
it("renders an empty current session without accepting unrelated or sessionless logs", async () => {
|
||||||
await renderDrawer();
|
await renderDrawer();
|
||||||
|
|
||||||
@@ -153,7 +168,9 @@ describe("ServerManagementTerminalDrawer", () => {
|
|||||||
const oldStream = logStream("stdout-old", "session-old", "process.stdout", "2026-08-01T00:00:00Z");
|
const oldStream = logStream("stdout-old", "session-old", "process.stdout", "2026-08-01T00:00:00Z");
|
||||||
oldStream.latestSeq = 900;
|
oldStream.latestSeq = 900;
|
||||||
apiMocks.listLogStreams.mockResolvedValue({ items: [oldStream], count: 1 });
|
apiMocks.listLogStreams.mockResolvedValue({ items: [oldStream], count: 1 });
|
||||||
apiMocks.queryLogStream.mockResolvedValue({ logStreamId: oldStream.id, entries: [logEntry(1, "selected historical output", "2026-08-01T00:00:01Z")], nextSeq: 1, latestSeq: 1 });
|
apiMocks.queryLogStream.mockImplementation((request: { logStreamId: string }) => Promise.resolve(request.logStreamId === oldStream.id
|
||||||
|
? { logStreamId: oldStream.id, entries: [logEntry(1, "selected historical output", "2026-08-01T00:00:01Z")], nextSeq: 1, latestSeq: 1 }
|
||||||
|
: { logStreamId: request.logStreamId, entries: [], nextSeq: 0, latestSeq: 0 }));
|
||||||
await renderDrawer();
|
await renderDrawer();
|
||||||
await emitSession("session-current");
|
await emitSession("session-current");
|
||||||
await emitStream(logStream("stdout-current", "session-current", "process.stdout"));
|
await emitStream(logStream("stdout-current", "session-current", "process.stdout"));
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, plu
|
|||||||
const initialHistoryPendingRef = useRef(false);
|
const initialHistoryPendingRef = useRef(false);
|
||||||
const liveSessionRef = useRef<string | null | undefined>(undefined);
|
const liveSessionRef = useRef<string | null | undefined>(undefined);
|
||||||
const historyRequestRef = useRef(0);
|
const historyRequestRef = useRef(0);
|
||||||
|
const liveTailLoadedRef = useRef(new Map<string, number>());
|
||||||
const quickCommands = useMemo(() => terminalQuickCommandsForPlugin(pluginId), [pluginId]);
|
const quickCommands = useMemo(() => terminalQuickCommandsForPlugin(pluginId), [pluginId]);
|
||||||
const supportsCommands = quickCommands.length > 0;
|
const supportsCommands = quickCommands.length > 0;
|
||||||
|
|
||||||
@@ -124,6 +125,22 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, plu
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const loadLiveStreamTail = useCallback((stream: LogStreamResponse) => {
|
||||||
|
const latestSeq = Number(stream.latestSeq);
|
||||||
|
if (!Number.isFinite(latestSeq) || latestSeq <= 0) return;
|
||||||
|
const loadedThrough = liveTailLoadedRef.current.get(stream.id) ?? 0;
|
||||||
|
if (loadedThrough >= latestSeq) return;
|
||||||
|
liveTailLoadedRef.current.set(stream.id, latestSeq);
|
||||||
|
void platformApiClient.queryLogStream({ logStreamId: stream.id, afterSeq: Math.max(0, latestSeq - terminalHistoryWindow), limit: terminalHistoryWindow }).then((response) => {
|
||||||
|
if (!eventBelongsToLiveSession(stream.logSessionId, liveSessionRef.current)) return;
|
||||||
|
appendLines(response.entries.map((entry) => terminalLineFromLog({ ...stream, latestSeq: response.latestSeq }, entry)));
|
||||||
|
lockTerminalFollow();
|
||||||
|
}).catch((error) => {
|
||||||
|
if (!eventBelongsToLiveSession(stream.logSessionId, liveSessionRef.current)) return;
|
||||||
|
appendLines([terminalSystemLine("warn", `当前会话尾部日志读取失败,等待后续实时输出:${error instanceof Error ? error.message : "未知错误"}`, "SYSTEM", `tail-error-${stream.id}`, serverTimeRef.current)]);
|
||||||
|
});
|
||||||
|
}, [appendLines, lockTerminalFollow]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
setStreams({ status: "loading" });
|
setStreams({ status: "loading" });
|
||||||
@@ -137,6 +154,7 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, plu
|
|||||||
setHistoryLines({ status: "idle" });
|
setHistoryLines({ status: "idle" });
|
||||||
setSelectedHistoryStreamId("");
|
setSelectedHistoryStreamId("");
|
||||||
liveSessionRef.current = undefined;
|
liveSessionRef.current = undefined;
|
||||||
|
liveTailLoadedRef.current.clear();
|
||||||
serverTimeRef.current = undefined;
|
serverTimeRef.current = undefined;
|
||||||
historyRequestRef.current += 1;
|
historyRequestRef.current += 1;
|
||||||
initialHistoryPendingRef.current = true;
|
initialHistoryPendingRef.current = true;
|
||||||
@@ -180,6 +198,7 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, plu
|
|||||||
if (!stream || !eventBelongsToLiveSession(stream.logSessionId, liveSessionRef.current)) return;
|
if (!stream || !eventBelongsToLiveSession(stream.logSessionId, liveSessionRef.current)) return;
|
||||||
ready = true;
|
ready = true;
|
||||||
setStreams((current) => ({ status: "ready", data: mergeLogStreams(current.status === "ready" ? current.data : [], stream) }));
|
setStreams((current) => ({ status: "ready", data: mergeLogStreams(current.status === "ready" ? current.data : [], stream) }));
|
||||||
|
loadLiveStreamTail(stream);
|
||||||
});
|
});
|
||||||
events.addEventListener("ready", () => {
|
events.addEventListener("ready", () => {
|
||||||
ready = true;
|
ready = true;
|
||||||
@@ -198,7 +217,7 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, plu
|
|||||||
if (!ready) setStreams({ status: "error", reason: "实时日志推送连接失败" });
|
if (!ready) setStreams({ status: "error", reason: "实时日志推送连接失败" });
|
||||||
};
|
};
|
||||||
return () => events.close();
|
return () => events.close();
|
||||||
}, [appendLines, lockTerminalFollow, open, serverId]);
|
}, [appendLines, loadLiveStreamTail, lockTerminalFollow, open, serverId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !historyOpen) return;
|
if (!open || !historyOpen) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user