Archived
fix: show complete chat message metadata
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@jmfederico/pi-web": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Show complete chat message dates and model identifiers in one consistent label, wrap rather than truncate expanded metadata, and let the clean touch info control collapse while it retains focus.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { chatQueuedMessageSections } from "./ChatView";
|
import { chatMessageMetadataLabel, chatQueuedMessageSections } from "./ChatView";
|
||||||
|
|
||||||
describe("chatQueuedMessageSections", () => {
|
describe("chatQueuedMessageSections", () => {
|
||||||
it("labels client-side pending-start sends separately from server queued messages", () => {
|
it("labels client-side pending-start sends separately from server queued messages", () => {
|
||||||
@@ -22,3 +22,16 @@ describe("chatQueuedMessageSections", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("chatMessageMetadataLabel", () => {
|
||||||
|
it("uses one full date and model label without a model prefix", () => {
|
||||||
|
const timestamp = "2026-07-10T19:15:30.000Z";
|
||||||
|
const formattedTimestamp = new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "medium" }).format(new Date(timestamp));
|
||||||
|
|
||||||
|
expect(chatMessageMetadataLabel({
|
||||||
|
role: "assistant",
|
||||||
|
parts: [],
|
||||||
|
meta: { timestamp, model: { provider: "provider", id: "model" } },
|
||||||
|
})).toBe(`${formattedTimestamp} · provider/model`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ import "./ConversationMeter";
|
|||||||
import "./FormattedText";
|
import "./FormattedText";
|
||||||
import "./ToolExecutionView";
|
import "./ToolExecutionView";
|
||||||
|
|
||||||
const shortTimestampFormatter = new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
const messageTimestampFormatter = new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "medium" });
|
||||||
const fullTimestampFormatter = new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "medium" });
|
|
||||||
|
|
||||||
const partialStreamNoticeBodies = [
|
const partialStreamNoticeBodies = [
|
||||||
"You opened this chat while the assistant was already replying. The complete answer will appear shortly.",
|
"You opened this chat while the assistant was already replying. The complete answer will appear shortly.",
|
||||||
@@ -52,6 +51,28 @@ export function chatQueuedMessageSections(clientQueued: QueuedSessionMessage[],
|
|||||||
].filter((section): section is QueuedMessageSection => section !== undefined);
|
].filter((section): section is QueuedMessageSection => section !== undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function chatMessageMetadataLabel(message: ChatLine): string {
|
||||||
|
const timestamp = message.meta?.timestamp;
|
||||||
|
const time = timestamp === undefined ? undefined : formatMessageTimestamp(timestamp);
|
||||||
|
const model = chatMessageModelLabel(message);
|
||||||
|
const parts = [time, model].filter((part): part is string => part !== undefined && part !== "");
|
||||||
|
return parts.length === 0 ? "No Pi message metadata available" : parts.join(" · ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMessageTimestamp(timestamp: string): string | undefined {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
if (!Number.isFinite(date.getTime())) return undefined;
|
||||||
|
return messageTimestampFormatter.format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
function chatMessageModelLabel(message: ChatLine): string | undefined {
|
||||||
|
const model = message.meta?.model;
|
||||||
|
if (model === undefined) return undefined;
|
||||||
|
const id = model.responseId ?? model.id;
|
||||||
|
if (id === undefined || id === "") return model.provider;
|
||||||
|
return model.provider !== undefined && model.provider !== "" ? `${model.provider}/${id}` : id;
|
||||||
|
}
|
||||||
|
|
||||||
@customElement("chat-view")
|
@customElement("chat-view")
|
||||||
export class ChatView extends LitElement {
|
export class ChatView extends LitElement {
|
||||||
@property({ attribute: false }) messages: ChatLine[] = [];
|
@property({ attribute: false }) messages: ChatLine[] = [];
|
||||||
@@ -84,7 +105,7 @@ export class ChatView extends LitElement {
|
|||||||
private groupedMessagesInput?: ChatLine[];
|
private groupedMessagesInput?: ChatLine[];
|
||||||
private groupedMessagesStart = 0;
|
private groupedMessagesStart = 0;
|
||||||
private groupedMessagesCache: ChatGroup[] = [];
|
private groupedMessagesCache: ChatGroup[] = [];
|
||||||
private readonly messageMetaCache = new WeakMap<ChatLine, { short: string; full: string }>();
|
private readonly messageMetaCache = new WeakMap<ChatLine, string>();
|
||||||
private readonly messageCopyTextCache = new WeakMap<ChatLine, string>();
|
private readonly messageCopyTextCache = new WeakMap<ChatLine, string>();
|
||||||
private partialStreamNoticeBody: string | undefined;
|
private partialStreamNoticeBody: string | undefined;
|
||||||
private lastScrollTop = 0;
|
private lastScrollTop = 0;
|
||||||
@@ -397,7 +418,7 @@ export class ChatView extends LitElement {
|
|||||||
<b class="label">${message.role}</b>
|
<b class="label">${message.role}</b>
|
||||||
<div class="msg-header-trailing">
|
<div class="msg-header-trailing">
|
||||||
${this.renderMessageActions(message, key)}
|
${this.renderMessageActions(message, key)}
|
||||||
<span class=${expanded ? "msg-meta expanded" : "msg-meta"} role="button" tabindex="0" title=${meta.full} aria-label=${meta.full} aria-expanded=${String(expanded)} @click=${() => { this.expandedMetaKey = expanded ? undefined : key; }} @keydown=${(event: KeyboardEvent) => { this.onMetaKeydown(event, key, expanded); }}>${meta.short}</span>
|
<span class=${expanded ? "msg-meta expanded" : "msg-meta"} role="button" tabindex="0" title=${meta} aria-label=${meta} aria-expanded=${String(expanded)} @click=${() => { this.expandedMetaKey = expanded ? undefined : key; }} @keydown=${(event: KeyboardEvent) => { this.onMetaKeydown(event, key, expanded); }}>${meta}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -448,38 +469,14 @@ export class ChatView extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private messageMetaLabel(message: ChatLine): { short: string; full: string } {
|
private messageMetaLabel(message: ChatLine): string {
|
||||||
const cached = this.messageMetaCache.get(message);
|
const cached = this.messageMetaCache.get(message);
|
||||||
if (cached !== undefined) return cached;
|
if (cached !== undefined) return cached;
|
||||||
const timestamp = message.meta?.timestamp;
|
const label = chatMessageMetadataLabel(message);
|
||||||
const model = this.modelLabel(message);
|
|
||||||
if (timestamp === undefined && model === undefined) {
|
|
||||||
const empty = { short: "no info", full: "No Pi message metadata available" };
|
|
||||||
this.messageMetaCache.set(message, empty);
|
|
||||||
return empty;
|
|
||||||
}
|
|
||||||
const time = timestamp === undefined ? undefined : this.formatTimestamp(timestamp);
|
|
||||||
const parts = [time?.short, model].filter((part): part is string => part !== undefined && part !== "");
|
|
||||||
const fullParts = [time?.full, model === undefined ? undefined : `Model: ${model}`].filter((part): part is string => part !== undefined && part !== "");
|
|
||||||
const label = { short: parts.join(" · "), full: fullParts.join(" · ") };
|
|
||||||
this.messageMetaCache.set(message, label);
|
this.messageMetaCache.set(message, label);
|
||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatTimestamp(timestamp: string): { short: string; full: string } | undefined {
|
|
||||||
const date = new Date(timestamp);
|
|
||||||
if (!Number.isFinite(date.getTime())) return undefined;
|
|
||||||
return { short: shortTimestampFormatter.format(date), full: fullTimestampFormatter.format(date) };
|
|
||||||
}
|
|
||||||
|
|
||||||
private modelLabel(message: ChatLine): string | undefined {
|
|
||||||
const model = message.meta?.model;
|
|
||||||
if (model === undefined) return undefined;
|
|
||||||
const id = model.responseId ?? model.id;
|
|
||||||
if (id === undefined || id === "") return model.provider;
|
|
||||||
return model.provider !== undefined && model.provider !== "" ? `${model.provider}/${id}` : id;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderPart(part: ChatPart, message?: ChatLine) {
|
private renderPart(part: ChatPart, message?: ChatLine) {
|
||||||
if (part.type === "text" && message?.role === "bash") return html`<pre class="part shell-output">${part.text}</pre>`;
|
if (part.type === "text" && message?.role === "bash") return html`<pre class="part shell-output">${part.text}</pre>`;
|
||||||
if (part.type === "text") return html`<formatted-text class="part" .text=${part.text}></formatted-text>`;
|
if (part.type === "text") return html`<formatted-text class="part" .text=${part.text}></formatted-text>`;
|
||||||
|
|||||||
@@ -327,22 +327,24 @@ export const chatStyles = css`
|
|||||||
.msg.bash > .msg-header { border-bottom-color: color-mix(in srgb, var(--pi-success) 35%, transparent); background: var(--pi-success-bg); }
|
.msg.bash > .msg-header { border-bottom-color: color-mix(in srgb, var(--pi-success) 35%, transparent); background: var(--pi-success-bg); }
|
||||||
.msg.skill > .msg-header { border-bottom-color: color-mix(in srgb, var(--pi-purple-border) 35%, transparent); background: var(--pi-purple-surface); }
|
.msg.skill > .msg-header { border-bottom-color: color-mix(in srgb, var(--pi-purple-border) 35%, transparent); background: var(--pi-purple-surface); }
|
||||||
.group-msg > .msg-header { position: sticky; top: -26px; z-index: 4; margin: -10px 0 8px; padding: 7px 0 6px; border-bottom: 1px solid color-mix(in srgb, var(--pi-border-muted) 35%, transparent); background: var(--pi-bg); }
|
.group-msg > .msg-header { position: sticky; top: -26px; z-index: 4; margin: -10px 0 8px; padding: 7px 0 6px; border-bottom: 1px solid color-mix(in srgb, var(--pi-border-muted) 35%, transparent); background: var(--pi-bg); }
|
||||||
.msg-header-trailing { min-width: 0; display: inline-flex; align-items: baseline; justify-content: flex-end; gap: 8px; }
|
.msg-header-trailing { min-width: 0; flex: 1 1 auto; display: inline-flex; align-items: baseline; justify-content: flex-end; gap: 8px; }
|
||||||
.msg-actions { display: inline-flex; gap: 6px; opacity: 0; transition: opacity .12s ease; }
|
.msg-actions { flex: 0 0 auto; display: inline-flex; gap: 6px; opacity: 0; transition: opacity .12s ease; }
|
||||||
.msg-action { display: inline-grid; place-items: center; width: 24px; height: 24px; border: 1px solid var(--pi-border); border-radius: 6px; background: var(--pi-surface); color: var(--pi-muted); padding: 0; font: 14px system-ui, sans-serif; line-height: 1; cursor: pointer; }
|
.msg-action { display: inline-grid; place-items: center; width: 24px; height: 24px; border: 1px solid var(--pi-border); border-radius: 6px; background: var(--pi-surface); color: var(--pi-muted); padding: 0; font: 14px system-ui, sans-serif; line-height: 1; cursor: pointer; }
|
||||||
.msg-action:hover, .msg-action:focus { color: var(--pi-text); border-color: var(--pi-accent); }
|
.msg-action:hover, .msg-action:focus { color: var(--pi-text); border-color: var(--pi-accent); }
|
||||||
.msg:hover > .msg-header .msg-actions, .msg:focus-within > .msg-header .msg-actions, .group-msg:hover > .msg-header .msg-actions, .group-msg:focus-within > .msg-header .msg-actions { opacity: 1; }
|
.msg:hover > .msg-header .msg-actions, .msg:focus-within > .msg-header .msg-actions, .group-msg:hover > .msg-header .msg-actions, .group-msg:focus-within > .msg-header .msg-actions { opacity: 1; }
|
||||||
.label { display: block; color: var(--pi-muted); font-size: 12px; text-transform: uppercase; }
|
.label { display: block; color: var(--pi-muted); font-size: 12px; text-transform: uppercase; }
|
||||||
.msg-header .label { margin: 0; }
|
.msg-header .label { margin: 0; }
|
||||||
.msg-meta { min-width: 0; opacity: .28; border: 0; background: transparent; color: var(--pi-dim); padding: 0; font: 11px system-ui, sans-serif; text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: opacity .12s ease, max-width .12s ease; cursor: pointer; user-select: text; -webkit-user-select: text; }
|
.msg-meta { min-width: 0; opacity: .28; border: 0; background: transparent; color: var(--pi-dim); padding: 0; font: 11px system-ui, sans-serif; text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: opacity .12s ease; cursor: pointer; user-select: text; -webkit-user-select: text; }
|
||||||
.msg:hover > .msg-header .msg-meta, .msg:focus-within > .msg-header .msg-meta, .group-msg:hover > .msg-header .msg-meta, .group-msg:focus-within > .msg-header .msg-meta, .msg-meta:focus, .msg-meta.expanded { opacity: 1; }
|
.msg:hover > .msg-header .msg-meta, .msg:focus-within > .msg-header .msg-meta, .group-msg:hover > .msg-header .msg-meta, .group-msg:focus-within > .msg-header .msg-meta, .msg-meta:focus, .msg-meta.expanded { opacity: 1; }
|
||||||
|
.msg-meta.expanded { flex: 1 1 auto; max-width: 100%; white-space: normal; overflow: visible; overflow-wrap: anywhere; text-overflow: clip; }
|
||||||
.msg-meta:focus { outline: 1px solid var(--pi-border); outline-offset: 3px; border-radius: 4px; }
|
.msg-meta:focus { outline: 1px solid var(--pi-border); outline-offset: 3px; border-radius: 4px; }
|
||||||
@media (hover: none) {
|
@media (hover: none) {
|
||||||
.msg-actions { opacity: 1; }
|
.msg-actions { opacity: 1; }
|
||||||
.msg-meta { opacity: .75; max-width: 26px; }
|
.msg-meta { opacity: .75; max-width: 26px; }
|
||||||
|
.msg-meta:not(.expanded) { display: inline-grid; width: 26px; height: 26px; place-items: center; font-size: 0; text-overflow: clip; }
|
||||||
.msg-meta::before { content: "ⓘ"; font-size: 13px; }
|
.msg-meta::before { content: "ⓘ"; font-size: 13px; }
|
||||||
.msg-meta:focus, .msg-meta.expanded { opacity: 1; max-width: 75%; }
|
.msg-meta.expanded { opacity: 1; max-width: 100%; }
|
||||||
.msg-meta:focus::before, .msg-meta.expanded::before { content: ""; }
|
.msg-meta.expanded::before { content: ""; }
|
||||||
}
|
}
|
||||||
formatted-text.part { display: block; }
|
formatted-text.part { display: block; }
|
||||||
formatted-text.part, .queued-message formatted-text { text-align: start; unicode-bidi: plaintext; }
|
formatted-text.part, .queued-message formatted-text { text-align: start; unicode-bidi: plaintext; }
|
||||||
|
|||||||
Reference in New Issue
Block a user