mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Show wikilink mentions in message details
This commit is contained in:
parent
4f22e5958a
commit
35d9c2ea47
8 changed files with 116 additions and 3 deletions
|
|
@ -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") ?? "") : "",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue