From 35d9c2ea479e0388dcfd8f46bc0f0ee1e98ee579 Mon Sep 17 00:00:00 2001 From: murashit Date: Wed, 20 May 2026 11:39:22 +0900 Subject: [PATCH] Show wikilink mentions in message details --- src/display/signature.ts | 1 + src/display/thread-items.ts | 17 ++++++++++++++- src/display/types.ts | 6 ++++++ src/panel/view.ts | 10 +++++++-- src/ui/message-stream.ts | 25 ++++++++++++++++++++++ styles.css | 5 +++++ tests/display/display-model.test.ts | 23 +++++++++++++++++++++ tests/ui/view-renderers.test.ts | 32 +++++++++++++++++++++++++++++ 8 files changed, 116 insertions(+), 3 deletions(-) diff --git a/src/display/signature.ts b/src/display/signature.ts index 90c6d68f..fe50516f 100644 --- a/src/display/signature.ts +++ b/src/display/signature.ts @@ -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") ?? "") : "", diff --git a/src/display/thread-items.ts b/src/display/thread-items.ts index 8ed309b2..2537e365 100644 --- a/src/display/thread-items.ts +++ b/src/display/thread-items.ts @@ -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(); + 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, diff --git a/src/display/types.ts b/src/display/types.ts index bbd750b7..744761de 100644 --- a/src/display/types.ts +++ b/src/display/types.ts @@ -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; } diff --git a/src/panel/view.ts b/src/panel/view.ts index 83197094..8eaa4e2f 100644 --- a/src/panel/view.ts +++ b/src/panel/view.ts @@ -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(); diff --git a/src/ui/message-stream.ts b/src/ui/message-stream.ts index 92472296..9bafa580 100644 --- a/src/ui/message-stream.ts +++ b/src/ui/message-stream.ts @@ -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, 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" }); diff --git a/styles.css b/styles.css index 161af2c9..be15d002 100644 --- a/styles.css +++ b/styles.css @@ -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; diff --git a/tests/display/display-model.test.ts b/tests/display/display-model.test.ts index e7aef6e8..d055be18 100644 --- a/tests/display/display-model.test.ts +++ b/tests/display/display-model.test.ts @@ -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]", diff --git a/tests/ui/view-renderers.test.ts b/tests/ui/view-renderers.test.ts index dfcfcfba..75dbc329 100644 --- a/tests/ui/view-renderers.test.ts +++ b/tests/ui/view-renderers.test.ts @@ -633,6 +633,38 @@ describe("message stream block identity and message actions", () => { expect(user?.querySelector(".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",