Show wikilink mentions in message details

This commit is contained in:
murashit 2026-05-20 11:39:22 +09:00
parent 4f22e5958a
commit 35d9c2ea47
8 changed files with 116 additions and 3 deletions

View file

@ -22,6 +22,7 @@ export function displayItemSignature(item: DisplayItem, context: DisplayItemSign
item.kind === "message" ? (item.copyText ?? "") : "",
item.kind === "message" ? String(isMessageCopyActionVisible(item, context)) : "",
item.kind === "message" ? String(item.proposedPlan ?? false) : "",
item.kind === "message" ? JSON.stringify(item.mentionedFiles ?? []) : "",
"output" in item ? (item.output ?? "") : "",
"details" in item ? JSON.stringify(item.details ?? []) : "",
item.kind === "message" ? (item.editedFiles?.join("\n") ?? "") : "",

View file

@ -1,6 +1,7 @@
import type { DisplayDetailSection, DisplayFileChange, DisplayItem } from "./types";
import type { DisplayDetailSection, DisplayFileChange, DisplayFileMention, DisplayItem } from "./types";
import type { ThreadItem } from "../generated/app-server/v2/ThreadItem";
import type { Turn } from "../generated/app-server/v2/Turn";
import type { UserInput } from "../generated/app-server/v2/UserInput";
import { inputToText, truncate } from "../utils";
import { referencedThreadDisplayFromPrompt } from "../threads/reference";
import { agentDisplayItem } from "./agent";
@ -87,6 +88,7 @@ export function displayItemFromThreadItem(item: ThreadItem, turnId?: string): Di
function userMessageDisplayItem(item: UserMessageItem, turnId?: string): DisplayItem {
const text = inputToText(item.content);
const mentionedFiles = fileMentionsFromInput(item.content);
const referencedThread = referencedThreadDisplayFromPrompt(text);
if (referencedThread) {
return {
@ -99,6 +101,7 @@ function userMessageDisplayItem(item: UserMessageItem, turnId?: string): Display
turnId,
itemId: item.id,
markdown: true,
...(mentionedFiles.length > 0 ? { mentionedFiles } : {}),
};
}
return {
@ -110,9 +113,21 @@ function userMessageDisplayItem(item: UserMessageItem, turnId?: string): Display
turnId,
itemId: item.id,
markdown: true,
...(mentionedFiles.length > 0 ? { mentionedFiles } : {}),
};
}
export function fileMentionsFromInput(input: UserInput[]): DisplayFileMention[] {
const seen = new Set<string>();
const mentions: DisplayFileMention[] = [];
for (const item of input) {
if (item.type !== "mention" || seen.has(item.path)) continue;
seen.add(item.path);
mentions.push({ name: item.name, path: item.path });
}
return mentions;
}
function agentMessageDisplayItem(item: AgentMessageItem, turnId?: string): DisplayItem {
return {
id: item.id,

View file

@ -42,6 +42,7 @@ export interface MessageDisplayItem extends DisplayBase {
role: "user" | "assistant";
copyText?: string;
referencedThread?: ReferencedThreadDisplay;
mentionedFiles?: DisplayFileMention[];
proposedPlan?: boolean;
editedFiles?: string[];
turnDiff?: DisplayTurnDiff;
@ -49,6 +50,11 @@ export interface MessageDisplayItem extends DisplayBase {
markdown?: boolean;
}
export interface DisplayFileMention {
name: string;
path: string;
}
export interface DisplayTurnDiff {
diff: string;
}

View file

@ -8,6 +8,7 @@ import type { SlashCommandName } from "../composer/slash-commands";
import { parseSlashCommand } from "../composer/suggestions";
import { VIEW_TYPE_CODEX_PANEL } from "../constants";
import { createSystemItem } from "../display/system";
import { fileMentionsFromInput } from "../display/thread-items";
import type { DisplayItem } from "../display/types";
import type { ReasoningEffort } from "../generated/app-server/ReasoningEffort";
import type { Thread } from "../generated/app-server/v2/Thread";
@ -395,6 +396,8 @@ export class CodexPanelView extends ItemView {
this.threadRename.resetThreadTurnPresence(false);
}
const codexInput = codexInputOverride ?? this.composerController.codexInput(text);
const mentionedFiles = fileMentionsFromInput(codexInput);
optimisticUserId = `local-user-${Date.now()}`;
this.state.displayItems.push({
id: optimisticUserId,
@ -403,6 +406,7 @@ export class CodexPanelView extends ItemView {
text,
copyText: text,
...(referencedThread ? { referencedThread } : {}),
...(mentionedFiles.length > 0 ? { mentionedFiles } : {}),
markdown: true,
});
this.state.pendingTurnStart = { anchorItemId: optimisticUserId, promptSubmitHookItemIds: [] };
@ -415,7 +419,6 @@ export class CodexPanelView extends ItemView {
if (turnSettings.warning) {
this.addSystemMessage(`${this.collaborationModeLabel()} mode is selected, but ${turnSettings.warning}`);
}
const codexInput = codexInputOverride ?? this.composerController.codexInput(text);
const activeThreadId = this.state.activeThreadId;
if (!activeThreadId) return;
const response = await client.startTurn(
@ -470,12 +473,14 @@ export class CodexPanelView extends ItemView {
const threadId = this.state.activeThreadId;
const expectedTurnId = this.state.activeTurnId;
const codexInput = codexInputOverride ?? this.composerController.codexInput(text);
const mentionedFiles = fileMentionsFromInput(codexInput);
this.composerController.setDraft("", { clearSuggestions: true });
this.syncComposerControls();
try {
await this.client.steerTurn(threadId, expectedTurnId, codexInputOverride ?? this.composerController.codexInput(text));
await this.client.steerTurn(threadId, expectedTurnId, codexInput);
this.state.displayItems.push({
id: `local-steer-${Date.now()}`,
kind: "message",
@ -484,6 +489,7 @@ export class CodexPanelView extends ItemView {
copyText: text,
turnId: expectedTurnId,
...(referencedThread ? { referencedThread } : {}),
...(mentionedFiles.length > 0 ? { mentionedFiles } : {}),
markdown: true,
});
this.forceMessagesToBottom();

View file

@ -238,6 +238,9 @@ function renderDisplayItem(parent: HTMLElement, item: DisplayItem, context: Mess
if (item.kind === "message" && item.referencedThread) {
renderReferencedThread(messageEl, item);
}
if (item.kind === "message" && item.mentionedFiles && item.mentionedFiles.length > 0) {
renderMentionedFiles(messageEl, item, context);
}
if (item.kind === "message" && item.autoReviewSummaries && item.autoReviewSummaries.length > 0) {
renderAutoReviewSummaries(messageEl, item.autoReviewSummaries);
}
@ -298,6 +301,28 @@ function renderEditedFiles(parent: HTMLElement, item: Extract<DisplayItem, { kin
}
}
function renderMentionedFiles(parent: HTMLElement, item: Extract<DisplayItem, { kind: "message" }>, context: MessageStreamContext): void {
const mentionedFiles = item.mentionedFiles ?? [];
const label = mentionedFiles.length === 1 ? "Mentioned 1 file" : `Mentioned ${mentionedFiles.length} files`;
const wrapper = parent.createDiv({ cls: "codex-panel__mentioned-files" });
const details = createRememberedDetails(
wrapper,
context.openDetails,
`${item.id}:mentioned-files`,
"codex-panel__mentioned-files-details",
label,
false,
context.onDetailsToggle,
);
const list = details.createEl("ul");
for (const file of mentionedFiles) {
const row = list.createEl("li");
row.createSpan({ text: file.name });
row.createSpan({ cls: "codex-panel__edited-files-separator", text: " · " });
row.createSpan({ text: file.path });
}
}
function renderAutoReviewSummaries(parent: HTMLElement, summaries: string[]): void {
const label = summaries.length === 1 ? "Auto-reviewed 1 request" : `Auto-reviewed ${summaries.length} requests`;
const details = parent.createEl("details", { cls: "codex-panel__auto-reviews" });

View file

@ -983,6 +983,7 @@
.codex-panel__edited-files,
.codex-panel__referenced-thread,
.codex-panel__mentioned-files,
.codex-panel__auto-reviews {
margin-top: 6px;
color: var(--text-muted);
@ -1003,6 +1004,10 @@
display: block;
}
.codex-panel__mentioned-files-details {
display: block;
}
.codex-panel__edited-files-summary {
display: inline-flex;
align-items: center;

View file

@ -58,6 +58,29 @@ describe("thread item conversion preserves app-server semantics", () => {
expect(displayItemFromThreadItem(assistantMessage)).toMatchObject({ role: "assistant", copyText: "world", markdown: true });
});
it("keeps resolved file mentions visible as user message metadata", () => {
const userMessage: ThreadItem = {
type: "userMessage",
id: "u1",
content: [
{ type: "text", text: "Read [[Alpha]] and [[Beta]].", text_elements: [] },
{ type: "mention", name: "Alpha", path: "thoughts/Alpha.md" },
{ type: "mention", name: "Alpha duplicate", path: "thoughts/Alpha.md" },
{ type: "mention", name: "Beta", path: "thoughts/Beta.md" },
],
};
expect(displayItemFromThreadItem(userMessage)).toMatchObject({
kind: "message",
role: "user",
text: "Read [[Alpha]] and [[Beta]].",
mentionedFiles: [
{ name: "Alpha", path: "thoughts/Alpha.md" },
{ name: "Beta", path: "thoughts/Beta.md" },
],
});
});
it("hides persisted /refer context in displayed user messages", () => {
const text = [
"[Codex Panel referenced thread]",

View file

@ -633,6 +633,38 @@ describe("message stream block identity and message actions", () => {
expect(user?.querySelector<HTMLElement>(".codex-panel__referenced-thread")?.title).toBe("thread-reference");
});
it("renders resolved file mentions as a collapsed user message attachment", () => {
const blocks = messageRenderBlocks({
activeThreadId: "thread",
activeTurnId: null,
historyCursor: null,
loadingHistory: false,
busy: false,
displayItems: [
{
id: "u1",
kind: "message",
role: "user",
text: "Read [[Alpha]].",
copyText: "Read [[Alpha]].",
markdown: true,
mentionedFiles: [{ name: "Alpha", path: "thoughts/Alpha.md" }],
},
],
openDetails: new Set(),
loadOlderTurns: vi.fn(),
renderMarkdown: (parent, text) => parent.createDiv({ text }),
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
});
const user = blocks.find((block) => block.key === "item:u1")?.render();
expect(user?.querySelector(".codex-panel__message-content")?.textContent).toBe("Read [[Alpha]].");
expect(user?.querySelector(".codex-panel__mentioned-files summary")?.textContent).toBe("Mentioned 1 file");
expect(user?.querySelector(".codex-panel__mentioned-files")?.textContent).toContain("Alpha");
expect(user?.querySelector(".codex-panel__mentioned-files")?.textContent).toContain("thoughts/Alpha.md");
});
it("does not render the open diff action without aggregated turn diff", () => {
const blocks = messageRenderBlocks({
activeThreadId: "thread",