[codex] Fix VS Code session previews (#3593)

* Fix Codex VS Code session previews

* fix(codex): use last IDE request heading for session previews

A markdown heading inside the active selection / open file could precede the real injected request, so matching the first "## My request for Codex:" heading picked selection content instead of the user prompt. Scan for the last matching heading (the IDE injects the real request as the final section) on both the Rust title path and the frontend TOC preview path.

Add regression tests for the selection-heading case, and pin the known best-effort limitation when the request body itself repeats the heading.

---------

Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
ayxwi
2026-06-07 19:23:24 +08:00
committed by GitHub
parent 2626eeebe6
commit 6716a4c408
4 changed files with 398 additions and 14 deletions
+21 -8
View File
@@ -48,12 +48,15 @@ import { SessionItem } from "./SessionItem";
import { SessionMessageItem } from "./SessionMessageItem";
import { SessionTocDialog, SessionTocSidebar } from "./SessionToc";
import {
extractCodexPromptPreview,
formatSessionMessagePreview,
formatSessionTitle,
formatTimestamp,
getBaseName,
getProviderIconName,
getProviderLabel,
getSessionKey,
shouldHideCodexMessageFromToc,
} from "./utils";
type ProviderFilter =
@@ -167,18 +170,28 @@ export function SessionManagerPage({ appId }: { appId: string }) {
});
}, [sessions]);
const isCodexSession = selectedSession?.providerId === "codex";
// 提取用户消息用于目录
const userMessagesToc = useMemo(() => {
return messages
.map((msg, index) => ({ msg, index }))
.filter(({ msg }) => msg.role.toLowerCase() === "user")
.map(({ msg, index }) => ({
index,
preview:
msg.content.slice(0, 50) + (msg.content.length > 50 ? "..." : ""),
ts: msg.ts,
}));
}, [messages]);
.filter(({ msg }) => {
if (msg.role.toLowerCase() !== "user") return false;
return !(isCodexSession && shouldHideCodexMessageFromToc(msg.content));
})
.map(({ msg, index }) => {
const previewContent = isCodexSession
? extractCodexPromptPreview(msg.content)
: msg.content;
return {
index,
preview: formatSessionMessagePreview(previewContent),
ts: msg.ts,
};
});
}, [isCodexSession, messages]);
const scrollToMessage = (index: number) => {
virtualizer.scrollToIndex(index, { align: "center", behavior: "smooth" });
+73
View File
@@ -2,6 +2,56 @@ import type { ReactNode } from "react";
import { createElement } from "react";
import { SessionMeta } from "@/types";
const CODEX_IDE_CONTEXT_PREFIX = "# Context from my IDE setup:";
const CODEX_REQUEST_MARKER = "my request for codex";
const getCodexRequestHeadingPayload = (lineText: string) => {
if (!lineText.startsWith("#")) return null;
const heading = lineText.replace(/^#+\s*/, "");
const suffix = heading.toLowerCase().startsWith(CODEX_REQUEST_MARKER)
? heading.slice(CODEX_REQUEST_MARKER.length).trimStart()
: null;
if (suffix === null) return null;
if (!suffix) return "";
if (!/^[:\-—]/.test(suffix)) return null;
return suffix.replace(/^[:\-—\s]+/, "").trim();
};
const extractCodexPromptFromIdeContext = (content: string) => {
const trimmed = content.trim();
if (!trimmed.startsWith(CODEX_IDE_CONTEXT_PREFIX)) {
return null;
}
// VS Code injects the real prompt as the LAST "## My request for Codex:"
// section, so keep the final matching heading. Earlier matches can be
// headings that live inside the active selection / open file content.
// Trade-off: if the request body itself repeats the heading, the preview
// truncates to its trailing part (rare; see sessionUtils.test.ts).
const lines = trimmed.replace(/\r\n/g, "\n").split("\n");
let prompt: string | null = null;
for (const [index, line] of lines.entries()) {
const inlinePrompt = getCodexRequestHeadingPayload(line.trim());
if (inlinePrompt === null) continue;
if (inlinePrompt) {
prompt = inlinePrompt;
continue;
}
const followingPrompt = lines
.slice(index + 1)
.join("\n")
.trim();
prompt = followingPrompt || null;
}
return prompt;
};
export const getSessionKey = (session: SessionMeta) =>
`${session.providerId}:${session.sessionId}:${session.sourcePath ?? ""}`;
@@ -81,6 +131,29 @@ export const formatSessionTitle = (session: SessionMeta) => {
);
};
export const shouldHideCodexMessageFromToc = (content: string) => {
const trimmed = content.trim();
return (
trimmed.startsWith("# AGENTS.md instructions for ") ||
trimmed.startsWith("<environment_context>") ||
(trimmed.startsWith(CODEX_IDE_CONTEXT_PREFIX) &&
!extractCodexPromptFromIdeContext(trimmed))
);
};
export const extractCodexPromptPreview = (content: string) => {
return extractCodexPromptFromIdeContext(content) ?? content;
};
export const formatSessionMessagePreview = (
content: string,
maxLength = 50,
) => {
return (
content.slice(0, maxLength) + (content.length > maxLength ? "..." : "")
);
};
export const highlightText = (text: string, query: string): ReactNode => {
if (!query) return text;
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");