Archived
feat(client): project ask_user transcript records
This commit is contained in:
@@ -290,7 +290,7 @@ function parseAskUserQuestionRecord(value: unknown): AskUserQuestionRecord {
|
||||
return { question, answered, values, ...(otherText === undefined ? {} : { otherText }) };
|
||||
}
|
||||
|
||||
function parseAskUserOutcome(value: unknown): AskUserOutcome {
|
||||
export function parseAskUserOutcome(value: unknown): AskUserOutcome {
|
||||
const record = requireRecord(value);
|
||||
const questions = boundedArrayOf(record["questions"], parseAskUserQuestionRecord, ASK_USER_QUESTION_LIMIT, "questions");
|
||||
const answeredCount = requireNonNegativeSafeInteger(record, "answeredCount");
|
||||
|
||||
@@ -67,6 +67,6 @@ function toolNameFromParts(parts: ChatPart[]): string | undefined {
|
||||
|
||||
function isReadablePart(message: ChatLine, part: ChatPart): boolean {
|
||||
if (message.source === "compaction" || message.source === "branch_summary") return false;
|
||||
if (part.type === "skillInvocation" || part.type === "skillRead" || part.type === "image") return true;
|
||||
if (part.type === "skillInvocation" || part.type === "skillRead" || part.type === "image" || part.type === "askUserRecord") return true;
|
||||
return part.type === "text" && (message.role === "user" || message.role === "assistant" || message.role === "system" || message.role === "bash");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,39 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ASK_USER_ANSWERS_CUSTOM_TYPE, type AskUserOutcome } from "../../shared/apiTypes";
|
||||
import { groupChatMessages } from "./chatGroups";
|
||||
import { appendText, appendThinking, normalizeMessage, normalizeMessages, textMessage } from "./chatMessages";
|
||||
|
||||
const askUserOutcome: AskUserOutcome = {
|
||||
askId: "ask-1",
|
||||
reason: "submitted",
|
||||
askedAt: "2026-07-20T10:00:00.000Z",
|
||||
closedAt: "2026-07-20T10:05:00.000Z",
|
||||
questions: [
|
||||
{
|
||||
question: { id: "db", question: "Which database?", options: [{ value: "pg", label: "Postgres" }] },
|
||||
answered: true,
|
||||
values: ["pg"],
|
||||
},
|
||||
{
|
||||
question: { id: "cache", question: "Which cache?", options: [{ value: "redis", label: "Redis" }] },
|
||||
answered: false,
|
||||
values: [],
|
||||
},
|
||||
],
|
||||
answeredCount: 1,
|
||||
unansweredIds: ["cache"],
|
||||
summary: "Answered 1 of 2; unanswered: cache",
|
||||
};
|
||||
|
||||
const supersededAskUserOutcome: AskUserOutcome = {
|
||||
...askUserOutcome,
|
||||
reason: "superseded",
|
||||
questions: askUserOutcome.questions.map((record) => ({ question: record.question, answered: false, values: [] })),
|
||||
answeredCount: 0,
|
||||
unansweredIds: ["db", "cache"],
|
||||
summary: "Answered 0 of 2; unanswered: db, cache",
|
||||
};
|
||||
|
||||
describe("chat message normalization", () => {
|
||||
it("normalizes simple text messages and drops empty content", () => {
|
||||
expect(normalizeMessages([
|
||||
@@ -20,6 +53,45 @@ describe("chat message normalization", () => {
|
||||
expect(normalizeMessages([{ role: "user", content: "raw" }, line])).toEqual([textMessage("user", "raw"), line]);
|
||||
});
|
||||
|
||||
it("projects ask_user answer messages into visible read-only record parts", () => {
|
||||
const normalized = normalizeMessage({
|
||||
role: "custom",
|
||||
customType: ASK_USER_ANSWERS_CUSTOM_TYPE,
|
||||
content: "model-facing answer text",
|
||||
details: askUserOutcome,
|
||||
});
|
||||
const recordLine = { role: "system" as const, parts: [{ type: "askUserRecord" as const, outcome: askUserOutcome }] };
|
||||
|
||||
expect(normalized).toEqual([recordLine]);
|
||||
expect(groupChatMessages(normalized)).toEqual([{ kind: "message", index: 0, message: recordLine }]);
|
||||
});
|
||||
|
||||
it("falls back to model-facing text when an ask_user answer record is malformed", () => {
|
||||
expect(normalizeMessage({
|
||||
role: "custom",
|
||||
customType: ASK_USER_ANSWERS_CUSTOM_TYPE,
|
||||
content: "Answered 0 of 1; unanswered: db",
|
||||
details: { askId: "missing-the-rest" },
|
||||
})).toEqual([textMessage("system", "Answered 0 of 1; unanswered: db")]);
|
||||
});
|
||||
|
||||
it("projects a superseded ask from the later ask_user tool result", () => {
|
||||
const normalized = normalizeMessages([
|
||||
{ role: "assistant", content: [{ type: "toolCall", id: "ask-call", name: "ask_user", arguments: { questions: [] } }] },
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "ask-call",
|
||||
toolName: "ask_user",
|
||||
content: [{ type: "text", text: "Posted a newer question set." }],
|
||||
details: { ask: { askId: "ask-2" }, superseded: supersededAskUserOutcome },
|
||||
isError: false,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(normalized[1]).toEqual({ role: "tool", parts: [{ type: "askUserRecord", outcome: supersededAskUserOutcome }] });
|
||||
expect(groupChatMessages(normalized).map((group) => group.kind)).toEqual(["group", "message"]);
|
||||
});
|
||||
|
||||
it("normalizes tool calls and tool results", () => {
|
||||
expect(normalizeMessage({ role: "assistant", content: [{ type: "toolCall", name: "bash", arguments: { command: "npm test" } }] })).toEqual([
|
||||
{ role: "assistant", parts: [{ type: "toolCall", toolName: "bash", summary: "npm test", args: { command: "npm test" } }] },
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ASK_USER_ANSWERS_CUSTOM_TYPE } from "../../shared/apiTypes";
|
||||
import { parseAskUserOutcome } from "./api/parsers";
|
||||
import type { ChatLine, ChatPart, ToolExecutionPart, ToolPreview } from "./components/shared";
|
||||
|
||||
export function normalizeMessages(messages: unknown[]): ChatLine[] {
|
||||
@@ -44,8 +46,13 @@ export function appendThinking(messages: ChatLine[], text: string): ChatLine[] {
|
||||
export function normalizeMessage(message: unknown): ChatLine[] {
|
||||
if (isChatLine(message)) return [message];
|
||||
if (getString(message, "role") === "bashExecution") return [withMessageMeta(normalizeBashExecution(message), message)];
|
||||
const role = normalizeRole(getString(message, "role"));
|
||||
const parts = normalizeContent(getProperty(message, "content"), message);
|
||||
const rawRole = getString(message, "role");
|
||||
const role = normalizeRole(rawRole);
|
||||
const contentParts = normalizeContent(getProperty(message, "content"), message);
|
||||
const supersededRecord = rawRole === "toolResult"
|
||||
? askUserRecordFromToolDetails(getString(message, "toolName") ?? "", getProperty(message, "details"))
|
||||
: undefined;
|
||||
const parts = supersededRecord === undefined ? contentParts : [...contentParts, supersededRecord];
|
||||
const skillLines = role === "user" ? normalizeSkillInvocation(parts) : undefined;
|
||||
if (skillLines !== undefined) return skillLines.map((line) => withMessageMeta(line, message));
|
||||
const source = normalizeSource(message);
|
||||
@@ -148,6 +155,8 @@ function normalizeRole(role: unknown): ChatLine["role"] {
|
||||
}
|
||||
|
||||
function normalizeContent(content: unknown, message: unknown): ChatPart[] {
|
||||
const askUserRecord = askUserRecordPart(message);
|
||||
if (askUserRecord !== undefined) return [askUserRecord];
|
||||
if (typeof content === "string") return content !== "" ? [{ type: "text", text: content }] : [];
|
||||
if (!Array.isArray(content)) return objectFallback(content);
|
||||
|
||||
@@ -179,6 +188,27 @@ function normalizeContent(content: unknown, message: unknown): ChatPart[] {
|
||||
: part);
|
||||
}
|
||||
|
||||
function askUserRecordPart(message: unknown): Extract<ChatPart, { type: "askUserRecord" }> | undefined {
|
||||
if (getString(message, "role") !== "custom" || getString(message, "customType") !== ASK_USER_ANSWERS_CUSTOM_TYPE) return undefined;
|
||||
return parsedAskUserRecord(getProperty(message, "details"));
|
||||
}
|
||||
|
||||
/** Project the superseded ask carried by an `ask_user` tool result, if any. */
|
||||
export function askUserRecordFromToolDetails(toolName: string, details: unknown): Extract<ChatPart, { type: "askUserRecord" }> | undefined {
|
||||
if (toolName !== "ask_user") return undefined;
|
||||
return parsedAskUserRecord(getProperty(details, "superseded"));
|
||||
}
|
||||
|
||||
function parsedAskUserRecord(value: unknown): Extract<ChatPart, { type: "askUserRecord" }> | undefined {
|
||||
try {
|
||||
return { type: "askUserRecord", outcome: parseAskUserOutcome(value) };
|
||||
} catch {
|
||||
// A malformed legacy/session entry must not make the whole transcript fail.
|
||||
// Fall back to its model-facing text through the ordinary normalizer.
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function toolResultPartFromText(text: string, message: unknown): Extract<ChatPart, { type: "toolResult" }> {
|
||||
const toolCallId = getString(message, "toolCallId");
|
||||
const content = getProperty(message, "content");
|
||||
|
||||
@@ -1,9 +1,41 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ASK_USER_ANSWERS_CUSTOM_TYPE, type AskUserOutcome } from "../../shared/apiTypes";
|
||||
import { groupChatMessages } from "./chatGroups";
|
||||
import { normalizeMessages, textMessage } from "./chatMessages";
|
||||
import { applyTranscriptEvent, seedStreamingPartial } from "./chatTranscript";
|
||||
import type { ChatLine } from "./components/shared";
|
||||
|
||||
const askUserOutcome: AskUserOutcome = {
|
||||
askId: "ask-1",
|
||||
reason: "submitted",
|
||||
askedAt: "2026-07-20T10:00:00.000Z",
|
||||
closedAt: "2026-07-20T10:05:00.000Z",
|
||||
questions: [
|
||||
{
|
||||
question: { id: "editor", question: "Which editor?", options: [{ value: "vim", label: "Vim" }] },
|
||||
answered: true,
|
||||
values: ["vim"],
|
||||
},
|
||||
{
|
||||
question: { id: "region", question: "Which region?", options: [{ value: "eu", label: "Europe" }] },
|
||||
answered: false,
|
||||
values: [],
|
||||
},
|
||||
],
|
||||
answeredCount: 1,
|
||||
unansweredIds: ["region"],
|
||||
summary: "Answered 1 of 2; unanswered: region",
|
||||
};
|
||||
|
||||
const supersededAskUserOutcome: AskUserOutcome = {
|
||||
...askUserOutcome,
|
||||
reason: "superseded",
|
||||
questions: askUserOutcome.questions.map((record) => ({ question: record.question, answered: false, values: [] })),
|
||||
answeredCount: 0,
|
||||
unansweredIds: ["editor", "region"],
|
||||
summary: "Answered 0 of 2; unanswered: editor, region",
|
||||
};
|
||||
|
||||
const finalAssistant = {
|
||||
role: "assistant",
|
||||
content: [
|
||||
@@ -27,6 +59,65 @@ describe("applyTranscriptEvent", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("projects finalized ask_user answers identically to rehydrated history", () => {
|
||||
const rawMessage = {
|
||||
role: "custom",
|
||||
customType: ASK_USER_ANSWERS_CUSTOM_TYPE,
|
||||
content: "The user submitted answers to your questions.",
|
||||
details: askUserOutcome,
|
||||
};
|
||||
const hydrated = normalizeMessages([rawMessage]);
|
||||
const live = applyTranscriptEvent([], { type: "message.end", message: rawMessage });
|
||||
|
||||
expect(live).toEqual(hydrated);
|
||||
expect(live).toEqual([{
|
||||
role: "system",
|
||||
parts: [{ type: "askUserRecord", outcome: askUserOutcome }],
|
||||
}]);
|
||||
expect(applyTranscriptEvent(live ?? [], { type: "message.end", message: rawMessage })).toEqual(live);
|
||||
|
||||
const nextOutcome = { ...askUserOutcome, askId: "ask-2" };
|
||||
const nextRawMessage = { ...rawMessage, details: nextOutcome };
|
||||
expect(applyTranscriptEvent(live ?? [], { type: "message.end", message: nextRawMessage })).toEqual([
|
||||
...hydrated,
|
||||
...normalizeMessages([nextRawMessage]),
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps superseded ask records identical across live tool events and hydrated history", () => {
|
||||
const args = { questions: [{ id: "next", question: "Try again?", options: [] }] };
|
||||
const details = { ask: { askId: "ask-2" }, superseded: supersededAskUserOutcome };
|
||||
const finalResult = {
|
||||
role: "toolResult",
|
||||
toolCallId: "ask-call",
|
||||
toolName: "ask_user",
|
||||
content: [{ type: "text", text: "Posted a newer question set." }],
|
||||
details,
|
||||
isError: false,
|
||||
};
|
||||
const hydrated = normalizeMessages([
|
||||
{ role: "assistant", content: [{ type: "toolCall", id: "ask-call", name: "ask_user", arguments: args }] },
|
||||
finalResult,
|
||||
]);
|
||||
let live: ChatLine[] = [];
|
||||
|
||||
live = applyTranscriptEvent(live, { type: "tool.start", toolName: "ask_user", toolCallId: "ask-call", summary: "", args }) ?? live;
|
||||
live = applyTranscriptEvent(live, {
|
||||
type: "tool.end",
|
||||
toolName: "ask_user",
|
||||
toolCallId: "ask-call",
|
||||
text: "Posted a newer question set.",
|
||||
content: finalResult.content,
|
||||
details,
|
||||
isError: false,
|
||||
}) ?? live;
|
||||
live = applyTranscriptEvent(live, { type: "message.end", message: finalResult }) ?? live;
|
||||
|
||||
expect(live).toEqual(hydrated);
|
||||
expect(live.filter((line) => line.parts.some((part) => part.type === "askUserRecord"))).toHaveLength(1);
|
||||
expect(groupChatMessages(live).map((group) => group.kind)).toEqual(["group", "message"]);
|
||||
});
|
||||
|
||||
it("replaces the streamed assistant message with the finalized history shape", () => {
|
||||
const streamed: ChatLine[] = [
|
||||
textMessage("user", "question"),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { appendText, appendThinking, normalizeMessage, normalizeMessages, previewFromDetails, summarizeArgs, textMessage } from "./chatMessages";
|
||||
import { appendText, appendThinking, askUserRecordFromToolDetails, normalizeMessage, normalizeMessages, previewFromDetails, summarizeArgs, textMessage } from "./chatMessages";
|
||||
import type { ChatLine, ToolExecutionPart } from "./components/shared";
|
||||
import { appendShellChunk, finalizeShellMessage, shellStartMessage } from "./shellMessages";
|
||||
import type { SessionUiEvent } from "./sessionSocket";
|
||||
@@ -82,12 +82,27 @@ function applyFinalMessage(messages: ChatLine[], rawMessage: unknown): ChatLine[
|
||||
function applyFinalLine(messages: ChatLine[], displayEnded: ChatLine): ChatLine[] {
|
||||
const skillReadIndexes = findMatchingSkillReadIndexes(messages, displayEnded);
|
||||
if (skillReadIndexes.length > 0) return replaceSkillReadLines(messages, skillReadIndexes, displayEnded);
|
||||
const askUserRecord = displayEnded.parts.find((part) => part.type === "askUserRecord");
|
||||
if (askUserRecord !== undefined) return reconcileFinalAskUserRecord(messages, displayEnded, askUserRecord);
|
||||
const last = messages.at(-1);
|
||||
if (last?.role !== displayEnded.role) return [...messages, displayEnded];
|
||||
if (displayEnded.role === "assistant" || sameMessageText(last, displayEnded)) return [...messages.slice(0, -1), displayEnded];
|
||||
return [...messages, displayEnded];
|
||||
}
|
||||
|
||||
function reconcileFinalAskUserRecord(
|
||||
messages: ChatLine[],
|
||||
displayEnded: ChatLine,
|
||||
record: Extract<ChatLine["parts"][number], { type: "askUserRecord" }>,
|
||||
): ChatLine[] {
|
||||
for (let index = messages.length - 1; index >= 0; index--) {
|
||||
if (lineHasAskUserRecord(messages[index], record)) {
|
||||
return [...messages.slice(0, index), displayEnded, ...messages.slice(index + 1)];
|
||||
}
|
||||
}
|
||||
return [...messages, displayEnded];
|
||||
}
|
||||
|
||||
function withoutToolCalls(message: ChatLine): ChatLine {
|
||||
return { ...message, parts: message.parts.filter((part) => part.type !== "toolCall") };
|
||||
}
|
||||
@@ -143,8 +158,9 @@ function finalizeToolExecution(messages: ChatLine[], result: ToolResultUpdate):
|
||||
...(preview === undefined ? {} : { preview }),
|
||||
};
|
||||
}, (line) => reconcileToolResultPresentation(line, presentation));
|
||||
if (updated !== messages) return updated;
|
||||
|
||||
let finalized = updated;
|
||||
if (updated === messages) {
|
||||
const preview = previewFromDetails(details);
|
||||
const part: ToolExecutionPart = {
|
||||
type: "toolExecution",
|
||||
@@ -157,7 +173,30 @@ function finalizeToolExecution(messages: ChatLine[], result: ToolResultUpdate):
|
||||
...(details === undefined ? {} : { details }),
|
||||
...(preview === undefined ? {} : { preview }),
|
||||
};
|
||||
return [...messages, reconcileToolResultPresentation({ role: "tool", parts: [part] }, presentation)];
|
||||
finalized = [...messages, reconcileToolResultPresentation({ role: "tool", parts: [part] }, presentation)];
|
||||
}
|
||||
return reconcileAskUserToolRecord(finalized, toolName, details, presentation.meta);
|
||||
}
|
||||
|
||||
function reconcileAskUserToolRecord(messages: ChatLine[], toolName: string, details: unknown, meta: ChatLine["meta"] | undefined): ChatLine[] {
|
||||
const record = askUserRecordFromToolDetails(toolName, details);
|
||||
if (record === undefined) return messages;
|
||||
for (let index = messages.length - 1; index >= 0; index--) {
|
||||
const line = messages[index];
|
||||
if (!lineHasAskUserRecord(line, record)) continue;
|
||||
if (meta === undefined || line === undefined) return messages;
|
||||
return [...messages.slice(0, index), { ...line, meta }, ...messages.slice(index + 1)];
|
||||
}
|
||||
return [...messages, { role: "tool", parts: [record], ...(meta === undefined ? {} : { meta }) }];
|
||||
}
|
||||
|
||||
function lineHasAskUserRecord(
|
||||
line: ChatLine | undefined,
|
||||
record: Extract<ChatLine["parts"][number], { type: "askUserRecord" }>,
|
||||
): boolean {
|
||||
return line?.parts.some((part) => part.type === "askUserRecord"
|
||||
&& part.outcome.askId === record.outcome.askId
|
||||
&& part.outcome.reason === record.outcome.reason) === true;
|
||||
}
|
||||
|
||||
function updateToolExecution(
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { AskUserOutcome } from "../../../shared/apiTypes";
|
||||
import { AskUserCard } from "./AskUserCard";
|
||||
import { ChatView } from "./ChatView";
|
||||
|
||||
afterEach(() => {
|
||||
document.body.replaceChildren();
|
||||
});
|
||||
|
||||
describe("ChatView ask_user transcript records", () => {
|
||||
it("renders a projected outcome as the read-only question card with the machine-scoped draft key", async () => {
|
||||
const outcome: AskUserOutcome = {
|
||||
askId: "ask-1",
|
||||
reason: "submitted",
|
||||
askedAt: "2026-07-20T10:00:00.000Z",
|
||||
closedAt: "2026-07-20T10:05:00.000Z",
|
||||
questions: [
|
||||
{
|
||||
question: { id: "editor", question: "Which editor?", options: [{ value: "vim", label: "Vim" }] },
|
||||
answered: true,
|
||||
values: ["vim"],
|
||||
},
|
||||
{
|
||||
question: { id: "region", question: "Which region?", options: [{ value: "eu", label: "Europe" }] },
|
||||
answered: false,
|
||||
values: [],
|
||||
},
|
||||
],
|
||||
answeredCount: 1,
|
||||
unansweredIds: ["region"],
|
||||
summary: "Answered 1 of 2; unanswered: region",
|
||||
};
|
||||
const view = new ChatView();
|
||||
view.sessionId = "session-1";
|
||||
view.askDraftSessionId = "remote-a:session-1";
|
||||
view.messages = [{ role: "system", parts: [{ type: "askUserRecord", outcome }] }];
|
||||
document.body.append(view);
|
||||
await view.updateComplete;
|
||||
|
||||
const card = requiredElement(view.shadowRoot?.querySelector("ask-user-card"), "ask_user record card");
|
||||
expect(card).toBeInstanceOf(AskUserCard);
|
||||
expect(card.outcome).toEqual(outcome);
|
||||
expect(card.ask).toBeUndefined();
|
||||
expect(card.draftSessionId).toBe("remote-a:session-1");
|
||||
await card.updateComplete;
|
||||
|
||||
const cardRoot = requiredElement(card.shadowRoot, "ask_user record shadow root");
|
||||
expect(cardRoot.textContent).toContain("Answers sent");
|
||||
expect(cardRoot.textContent).toContain("Vim");
|
||||
expect(cardRoot.textContent).toContain("Which region?");
|
||||
expect(cardRoot.textContent).toContain("Unanswered");
|
||||
expect(cardRoot.querySelector("input, textarea, button, select")).toBeNull();
|
||||
expect(view.shadowRoot?.querySelector("article.ask-user-record-shell .msg-header")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
function requiredElement<T>(value: T | null | undefined, label: string): T {
|
||||
if (value === null || value === undefined) throw new Error(`Expected ${label}`);
|
||||
return value;
|
||||
}
|
||||
@@ -730,10 +730,12 @@ export class ChatView extends LitElement {
|
||||
|
||||
private renderMessage(message: ChatLine, index: number) {
|
||||
const toolOnly = this.isToolExecutionOnlyMessage(message);
|
||||
const askUserRecordOnly = this.isAskUserRecordOnlyMessage(message);
|
||||
const shellClass = toolOnly ? "msg tool-execution-shell" : "msg ask-user-record-shell";
|
||||
return html`
|
||||
${this.renderScrollMarker(this.messageScrollMarkerId(index))}
|
||||
<article class=${toolOnly ? "msg tool-execution-shell" : `msg ${message.role}`} data-index=${index} data-scroll-anchor-id=${this.messageAnchorKey(index)}>
|
||||
${toolOnly ? null : this.renderMessageHeader(message, String(index))}
|
||||
<article class=${toolOnly || askUserRecordOnly ? shellClass : `msg ${message.role}`} data-index=${index} data-scroll-anchor-id=${this.messageAnchorKey(index)}>
|
||||
${toolOnly || askUserRecordOnly ? null : this.renderMessageHeader(message, String(index))}
|
||||
${message.parts.map((part) => this.renderPart(part, message))}
|
||||
</article>
|
||||
`;
|
||||
@@ -754,6 +756,10 @@ export class ChatView extends LitElement {
|
||||
return message.role === "tool" && message.parts.length > 0 && message.parts.every((part) => part.type === "toolExecution");
|
||||
}
|
||||
|
||||
private isAskUserRecordOnlyMessage(message: ChatLine): boolean {
|
||||
return message.parts.length > 0 && message.parts.every((part) => part.type === "askUserRecord");
|
||||
}
|
||||
|
||||
private renderMessageGroup(messages: ChatLine[], startIndex: number, endIndex: number, defaultOpen: boolean) {
|
||||
const disclosureKey = this.groupDisclosureKey(startIndex, endIndex, defaultOpen);
|
||||
const open = this.disclosures.isOpen(disclosureKey, defaultOpen);
|
||||
@@ -873,6 +879,13 @@ export class ChatView extends LitElement {
|
||||
<small>read ${part.path}</small>
|
||||
</div>
|
||||
`;
|
||||
if (part.type === "askUserRecord") return html`
|
||||
<ask-user-card
|
||||
class="part"
|
||||
.outcome=${part.outcome}
|
||||
.draftSessionId=${this.askDraftSessionId}
|
||||
></ask-user-card>
|
||||
`;
|
||||
if (part.type === "image") {
|
||||
const { src, alt } = chatImagePartSource(part);
|
||||
return html`<img class="part chat-image" src=${src} alt=${alt} loading="lazy" role="button" tabindex="0" title="Click to enlarge" @load=${this.onImageLoad} @click=${() => { this.openImageZoom(src, alt); }} @keydown=${(event: KeyboardEvent) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); this.openImageZoom(src, alt); } }} />`;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { css, svg, type TemplateResult } from "lit";
|
||||
import type { AskUserOutcome } from "../../../shared/apiTypes";
|
||||
import type { SessionWarningSeverity } from "../api";
|
||||
|
||||
export function renderSessionWarningIcon(severity: SessionWarningSeverity, className: string): TemplateResult {
|
||||
@@ -54,6 +55,7 @@ export type ChatPart =
|
||||
| { type: "thinking"; text: string }
|
||||
| { type: "skillInvocation"; name: string; location: string; content: string }
|
||||
| { type: "skillRead"; name: string; path: string; toolCallId?: string }
|
||||
| { type: "askUserRecord"; outcome: AskUserOutcome }
|
||||
| { type: "toolCall"; toolCallId?: string; toolName: string; summary: string; args?: unknown }
|
||||
| ToolExecutionPart
|
||||
| { type: "toolResult"; toolCallId?: string; toolName: string; text: string; isError: boolean; content?: unknown; details?: unknown }
|
||||
@@ -374,7 +376,8 @@ export const chatStyles = css`
|
||||
.msg.assistant, .msg.tool-image-output { background: var(--pi-surface); }
|
||||
.msg.user { border-color: var(--pi-accent-border); background: var(--pi-selection-bg); }
|
||||
.msg.tool { border-color: var(--pi-warning-border); background: var(--pi-warning-surface); color: var(--pi-warning); }
|
||||
.msg.tool-execution-shell { padding: 0; border: 0; background: transparent; color: var(--pi-text); }
|
||||
.msg.tool-execution-shell, .msg.ask-user-record-shell { padding: 0; border: 0; background: transparent; color: var(--pi-text); }
|
||||
.msg.ask-user-record-shell ask-user-card { margin: 0 auto; }
|
||||
.msg.system { color: var(--pi-danger); }
|
||||
.msg.bash { border-color: var(--pi-success); background: var(--pi-success-bg); }
|
||||
.msg.skill { border-color: var(--pi-purple-border); background: var(--pi-purple-surface); }
|
||||
|
||||
Reference in New Issue
Block a user