fix: show dismissed question content in chat history (#10361)

This commit is contained in:
Thomas Brugman
2026-07-08 16:46:46 +02:00
committed by marius-kilocode
parent b27c1dd832
commit 8ff2a163af
25 changed files with 284 additions and 25 deletions
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Fixed dismissed question tool content not showing in chat history. Dismissed questions now render with a "Dismissed" label and "N dismissed" subtitle instead of being invisible.
@@ -1159,6 +1159,12 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
const i18n = useI18n()
const part = props.part as ToolPart
const hideQuestion = createMemo(() => part.tool === "question" && busy(part.state.status))
const isDismissedQuestionError = createMemo(() => {
if (part.tool !== "question") return false
if (part.state.status !== "error" || !part.state.error) return false
const errStr = typeof part.state.error === "string" ? part.state.error : ""
return errStr.includes("dismissed this question")
})
const emptyInput: Record<string, any> = {}
const emptyMetadata: Record<string, any> = {}
@@ -1177,13 +1183,24 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
<Match when={part.state.status === "error" && part.state.error}>
{(error) => {
const cleaned = error().replace("Error: ", "")
if (part.tool === "question" && cleaned.includes("dismissed this question")) {
if (isDismissedQuestionError()) {
return (
<div style="width: 100%; display: flex; justify-content: flex-end;">
<span class="text-13-regular text-text-weak cursor-default">
{i18n.t("ui.messagePart.questions.dismissed")}
</span>
</div>
<Dynamic
component={render()}
input={input()}
tool={part.tool}
partID={part.id}
callID={part.callID}
metadata={meta()}
partMetadata={top()}
// @ts-expect-error
output={part.state.output}
status={part.state.status}
hideDetails={props.hideDetails}
defaultOpen={props.defaultOpen}
animate
reveal={props.animate}
/>
)
}
const hint =
@@ -2827,12 +2844,15 @@ ToolRegistry.register({
const i18n = useI18n()
const questions = createMemo(() => (props.input.questions ?? []) as QuestionInfo[])
const answers = createMemo(() => (props.metadata.answers ?? []) as QuestionAnswer[])
const dismissed = createMemo(() => props.metadata.dismissed === true || props.status === "error")
const completed = createMemo(() => answers().length > 0)
const pending = createMemo(() => busy(props.status))
const hasContent = createMemo(() => completed() || dismissed())
const subtitle = createMemo(() => {
const count = questions().length
if (count === 0) return ""
if (dismissed()) return i18n.t("ui.question.subtitle.dismissed", { count })
if (completed()) return i18n.t("ui.question.subtitle.answered", { count })
return `${count} ${i18n.t(count > 1 ? "ui.common.question.other" : "ui.common.question.one")}`
})
@@ -2851,15 +2871,19 @@ ToolRegistry.register({
/>
}
>
<Show when={completed()}>
<div data-component="question-answers">
<Show when={hasContent()}>
<div data-component="question-answers" data-dismissed={dismissed() ? "" : undefined}>
<For each={questions()}>
{(q, i) => {
const answer = () => answers()[i()] ?? []
const answerText = () => {
if (dismissed()) return i18n.t("ui.question.answer.dismissed")
return answer().join(", ") || i18n.t("ui.question.answer.none")
}
return (
<div data-slot="question-answer-item">
<div data-slot="question-text">{q.question}</div>
<div data-slot="answer-text">{answer().join(", ") || i18n.t("ui.question.answer.none")}</div>
<div data-slot="answer-text">{answerText()}</div>
</div>
)
}}
@@ -529,6 +529,86 @@ const hintErrors: ToolPart[] = [
const mockDataHintErrors = createMockData(hintErrors)
// --- Question tool: answered (reference) ---
const questionAnsweredPart: ToolPart = {
id: "part-question-answered",
sessionID: SESSION_ID,
messageID: ASST_MSG_ID,
type: "tool",
callID: "call-question-answered",
tool: "question",
state: {
status: "completed",
input: {
questions: [
{
question: "Should I continue with this approach?",
header: "Continue?",
options: [
{ label: "Yes", description: "Proceed with the current plan" },
{ label: "No", description: "Stop and reconsider" },
],
},
{
question: "Which library should I use for date formatting?",
header: "Library",
options: [
{ label: "date-fns", description: "Lightweight, tree-shakeable" },
{ label: "luxon", description: "Full-featured DateTime library" },
{ label: "dayjs", description: "Moment.js compatible, 2kB" },
],
},
],
},
output: 'User answered: "Should I continue?"="Yes", "Which library?"="date-fns"',
title: "Asked 2 questions",
metadata: { answers: [["Yes"], ["date-fns"]] },
time: { start: now - 8000, end: now - 7000 },
},
}
// --- Question tool: dismissed (exercises the fix) ---
const questionDismissedPart: ToolPart = {
id: "part-question-dismissed",
sessionID: SESSION_ID,
messageID: ASST_MSG_ID,
type: "tool",
callID: "call-question-dismissed",
tool: "question",
state: {
status: "completed",
input: {
questions: [
{
question: "Should I continue with this approach?",
header: "Continue?",
options: [
{ label: "Yes", description: "Proceed with the current plan" },
{ label: "No", description: "Stop and reconsider" },
],
},
{
question: "Which library should I use for date formatting?",
header: "Library",
options: [
{ label: "date-fns", description: "Lightweight, tree-shakeable" },
{ label: "luxon", description: "Full-featured DateTime library" },
],
},
],
},
output: "User dismissed the question.",
title: "Question dismissed",
metadata: { answers: [], dismissed: true },
time: { start: now - 8000, end: now - 7000 },
},
}
const mockDataQuestionAnswered = createMockData([questionAnsweredPart, textPart])
const mockDataQuestionDismissed = createMockData([questionDismissedPart, textPart])
export const ToolHintErrors: Story = {
render: () => (
<AllProviders data={mockDataHintErrors}>
@@ -536,3 +616,59 @@ export const ToolHintErrors: Story = {
</AllProviders>
),
}
// --- Question tool: answered (collapsed) ---
export const QuestionAnswered: Story = {
name: "QuestionAnswered",
render: () => (
<AllProviders data={mockDataQuestionAnswered}>
<AssistantParts messages={[mockAssistantMessage]} />
</AllProviders>
),
}
// --- Question tool: answered (expanded) ---
export const QuestionAnsweredExpanded: Story = {
name: "QuestionAnswered (expanded)",
render: () => (
<AllProviders data={mockDataQuestionAnswered}>
<AssistantParts messages={[mockAssistantMessage]} />
</AllProviders>
),
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
const trigger = canvasElement
.querySelector('[data-slot="basic-tool-tool-title"]')
?.closest("button")
if (trigger) trigger.click()
},
}
// --- Question tool: dismissed (collapsed — "2 dismissed" subtitle) ---
export const QuestionDismissed: Story = {
name: "QuestionDismissed",
render: () => (
<AllProviders data={mockDataQuestionDismissed}>
<AssistantParts messages={[mockAssistantMessage]} />
</AllProviders>
),
}
// --- Question tool: dismissed (expanded — shows questions with "Dismissed" labels) ---
export const QuestionDismissedExpanded: Story = {
name: "QuestionDismissed (expanded)",
render: () => (
<AllProviders data={mockDataQuestionDismissed}>
<AssistantParts messages={[mockAssistantMessage]} />
</AllProviders>
),
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
const trigger = canvasElement
.querySelector('[data-slot="basic-tool-tool-title"]')
?.closest("button")
if (trigger) trigger.click()
},
}
@@ -1010,22 +1010,40 @@ function Question(props: ToolProps) {
arrayValue(props.input.questions).flatMap((item) => (isRecord(item) ? [item] : [])),
)
const answers = createMemo(() => arrayValue(props.metadata.answers))
// kilocode_change start - show dismissed question content; use questions()
// presence (not answers) so dismissed/answered/error states all render content.
const dismissed = createMemo(
() =>
props.metadata.dismissed === true ||
(props.part.state.status === "error" && String(props.part.state.error?.message ?? "").includes("dismissed")),
)
function format(answer: unknown) {
if (dismissed()) return "Dismissed"
return formatAnswer(answer)
}
const title = createMemo(() => (dismissed() ? "# Questions (dismissed)" : "# Questions"))
// kilocode_change end
return (
<Switch>
<Match when={answers().length > 0}>
<BlockTool title="# Questions" part={props.part}>
{/* kilocode_change start - gate on dismissed or answers so dismissed/answered render, pending falls through to Asking... */}
<Match when={dismissed() || answers().length > 0}>
<BlockTool title={title()} part={props.part}>
<box gap={1}>
<For each={questions()}>
{(question, index) => (
<box>
<text fg={theme.textMuted}>{stringValue(question.question)}</text>
<text fg={theme.text}>{formatAnswer(answers()[index()])}</text>
<text fg={theme.text}>{format(answers()[index()])}</text>
</box>
)}
</For>
</box>
</BlockTool>
</Match>
{/* kilocode_change end */}
<Match when={true}>
<InlineTool icon="→" pending="Asking questions..." complete={questions().length} part={props.part}>
Asked {questions().length} question{questions().length === 1 ? "" : "s"}
@@ -2815,28 +2815,64 @@ function TodoWrite(props: ToolProps<typeof TodoWriteTool>) {
function Question(props: ToolProps<typeof QuestionTool>) {
const { theme } = useTheme()
const count = createMemo(() => props.input.questions?.length ?? 0)
// kilocode_change start - show dismissed question content with toggle;
// use input.questions presence (not metadata) so dismissed/answered/error
// states all render content. Clicking the one-liner expands to the full
// block; clicking the block title collapses back.
const dismissed = createMemo(
() =>
props.metadata.dismissed === true ||
(props.part.state.status === "error" && String(props.part.state.error ?? "").includes("dismissed")),
)
const [expanded, setExpanded] = createSignal(false)
function format(answer?: ReadonlyArray<string>) {
if (dismissed()) return "Dismissed"
if (!answer?.length) return "(no answer)"
return answer.join(", ")
}
const title = createMemo(() => (dismissed() ? "# Questions (dismissed)" : "# Questions"))
const subtitle = createMemo(() => {
if (dismissed()) return `${count()} dismissed`
if ((props.metadata.answers?.length ?? 0) > 0) return `${count()} answered`
return `${count()} question${count() !== 1 ? "s" : ""}`
})
// kilocode_change end
return (
<Switch>
<Match when={props.metadata.answers}>
<BlockTool title="# Questions" part={props.part}>
<box gap={1}>
<For each={props.input.questions ?? []}>
{(q, i) => (
<box flexDirection="column">
<text fg={theme.textMuted}>{q.question}</text>
<text fg={theme.text}>{format(props.metadata.answers?.[i()])}</text>
</box>
)}
</For>
</box>
</BlockTool>
{/* kilocode_change start - toggle between one-liner and full block */}
<Match when={count() > 0}>
<Show
when={expanded()}
fallback={
<InlineTool
icon="→"
complete={count()}
pending="Asking questions..."
part={props.part}
onClick={() => setExpanded(true)}
>
{subtitle()}
</InlineTool>
}
>
<BlockTool title={title()} part={props.part} onClick={() => setExpanded(false)}>
<box gap={1}>
<For each={props.input.questions ?? []}>
{(q, i) => (
<box flexDirection="column">
<text fg={theme.textMuted}>{q.question}</text>
<text fg={theme.text}>{format(props.metadata.answers?.[i()])}</text>
</box>
)}
</For>
</box>
</BlockTool>
</Show>
</Match>
{/* kilocode_change end */}
<Match when={true}>
<InlineTool icon="→" pending="Asking questions..." complete={count()} part={props.part}>
Asked {count()} question{count() !== 1 ? "s" : ""}
+2
View File
@@ -172,7 +172,9 @@ export const dict = {
"ui.patch.action.patched": "مصحح",
"ui.question.subtitle.answered": "{{count}} أجيب",
"ui.question.subtitle.dismissed": "{{count}} dismissed", // kilocode_change
"ui.question.answer.none": "(لا توجد إجابة)",
"ui.question.answer.dismissed": "Dismissed", // kilocode_change
"ui.question.review.notAnswered": "(لم يتم الرد)",
"ui.question.multiHint": "حدد كل ما ينطبق",
"ui.question.singleHint": "حدد إجابة واحدة",
+2
View File
@@ -172,7 +172,9 @@ export const dict = {
"ui.patch.action.patched": "Patch aplicado",
"ui.question.subtitle.answered": "{{count}} respondidas",
"ui.question.subtitle.dismissed": "{{count}} dismissed", // kilocode_change
"ui.question.answer.none": "(sem resposta)",
"ui.question.answer.dismissed": "Dismissed", // kilocode_change
"ui.question.review.notAnswered": "(não respondida)",
"ui.question.multiHint": "Selecione todas que se aplicam",
"ui.question.singleHint": "Selecione uma resposta",
+2
View File
@@ -176,7 +176,9 @@ export const dict = {
"ui.patch.action.patched": "Primijenjeno",
"ui.question.subtitle.answered": "{{count}} odgovoreno",
"ui.question.subtitle.dismissed": "{{count}} dismissed", // kilocode_change
"ui.question.answer.none": "(nema odgovora)",
"ui.question.answer.dismissed": "Dismissed", // kilocode_change
"ui.question.review.notAnswered": "(nije odgovoreno)",
"ui.question.multiHint": "Odaberi sve što važi",
"ui.question.singleHint": "Odaberi jedan odgovor",
+2
View File
@@ -171,7 +171,9 @@ export const dict = {
"ui.patch.action.patched": "Patchet",
"ui.question.subtitle.answered": "{{count}} besvaret",
"ui.question.subtitle.dismissed": "{{count}} dismissed", // kilocode_change
"ui.question.answer.none": "(intet svar)",
"ui.question.answer.dismissed": "Dismissed", // kilocode_change
"ui.question.review.notAnswered": "(ikke besvaret)",
"ui.question.multiHint": "Vælg alle der gælder",
"ui.question.singleHint": "Vælg ét svar",
+2
View File
@@ -177,7 +177,9 @@ export const dict = {
"ui.patch.action.patched": "Gepatched",
"ui.question.subtitle.answered": "{{count}} beantwortet",
"ui.question.subtitle.dismissed": "{{count}} dismissed", // kilocode_change
"ui.question.answer.none": "(keine Antwort)",
"ui.question.answer.dismissed": "Dismissed", // kilocode_change
"ui.question.review.notAnswered": "(nicht beantwortet)",
"ui.question.multiHint": "Alle zutreffenden auswählen",
"ui.question.singleHint": "Eine Antwort auswählen",
+2
View File
@@ -185,7 +185,9 @@ export const dict: Record<string, string> = {
"ui.patch.action.patched": "Patched",
"ui.question.subtitle.answered": "{{count}} answered",
"ui.question.subtitle.dismissed": "{{count}} dismissed", // kilocode_change
"ui.question.answer.none": "(no answer)",
"ui.question.answer.dismissed": "Dismissed", // kilocode_change
"ui.question.review.notAnswered": "(not answered)",
"ui.question.multiHint": "Select all answers that apply",
"ui.question.singleHint": "Select one answer",
+2
View File
@@ -172,7 +172,9 @@ export const dict = {
"ui.patch.action.patched": "Parcheado",
"ui.question.subtitle.answered": "{{count}} respondidas",
"ui.question.subtitle.dismissed": "{{count}} dismissed", // kilocode_change
"ui.question.answer.none": "(sin respuesta)",
"ui.question.answer.dismissed": "Dismissed", // kilocode_change
"ui.question.review.notAnswered": "(no respondida)",
"ui.question.multiHint": "Selecciona todas las que correspondan",
"ui.question.singleHint": "Selecciona una respuesta",
+2
View File
@@ -172,7 +172,9 @@ export const dict = {
"ui.patch.action.patched": "Corrigé",
"ui.question.subtitle.answered": "{{count}} répondu(s)",
"ui.question.subtitle.dismissed": "{{count}} dismissed", // kilocode_change
"ui.question.answer.none": "(pas de réponse)",
"ui.question.answer.dismissed": "Dismissed", // kilocode_change
"ui.question.review.notAnswered": "(non répondu)",
"ui.question.multiHint": "Sélectionnez tout ce qui s'applique",
"ui.question.singleHint": "Sélectionnez une réponse",
+2
View File
@@ -187,7 +187,9 @@ export const dict: Record<string, string> = {
"ui.patch.action.patched": "Patch applicata",
"ui.question.subtitle.answered": "{{count}} risposte",
"ui.question.subtitle.dismissed": "{{count}} dismissed", // kilocode_change
"ui.question.answer.none": "(nessuna risposta)",
"ui.question.answer.dismissed": "Dismissed", // kilocode_change
"ui.question.review.notAnswered": "(senza risposta)",
"ui.question.multiHint": "Seleziona tutte le risposte applicabili",
"ui.question.singleHint": "Seleziona una risposta",
+2
View File
@@ -171,7 +171,9 @@ export const dict = {
"ui.patch.action.patched": "パッチ適用済み",
"ui.question.subtitle.answered": "{{count}}件回答済み",
"ui.question.subtitle.dismissed": "{{count}} dismissed", // kilocode_change
"ui.question.answer.none": "(回答なし)",
"ui.question.answer.dismissed": "Dismissed", // kilocode_change
"ui.question.review.notAnswered": "(未回答)",
"ui.question.multiHint": "該当するものをすべて選択",
"ui.question.singleHint": "1 つ選択",
+2
View File
@@ -172,7 +172,9 @@ export const dict = {
"ui.patch.action.patched": "패치됨",
"ui.question.subtitle.answered": "{{count}}개 답변됨",
"ui.question.subtitle.dismissed": "{{count}} dismissed", // kilocode_change
"ui.question.answer.none": "(답변 없음)",
"ui.question.answer.dismissed": "Dismissed", // kilocode_change
"ui.question.review.notAnswered": "(답변되지 않음)",
"ui.question.multiHint": "해당하는 항목 모두 선택",
"ui.question.singleHint": "하나의 답변을 선택",
+2
View File
@@ -190,7 +190,9 @@ export const dict: Record<string, string> = {
"ui.patch.action.patched": "Gepatcht",
"ui.question.subtitle.answered": "{{count}} beantwoord",
"ui.question.subtitle.dismissed": "{{count}} dismissed", // kilocode_change
"ui.question.answer.none": "(geen antwoord)",
"ui.question.answer.dismissed": "Dismissed", // kilocode_change
"ui.question.review.notAnswered": "(niet beantwoord)",
"ui.question.multiHint": "Selecteer alle antwoorden die van toepassing zijn",
"ui.question.singleHint": "Selecteer één antwoord",
+2
View File
@@ -175,7 +175,9 @@ export const dict: Record<Keys, string> = {
"ui.patch.action.patched": "Oppdatert",
"ui.question.subtitle.answered": "{{count}} besvart",
"ui.question.subtitle.dismissed": "{{count}} dismissed", // kilocode_change
"ui.question.answer.none": "(ingen svar)",
"ui.question.answer.dismissed": "Dismissed", // kilocode_change
"ui.question.review.notAnswered": "(ikke besvart)",
"ui.question.multiHint": "Velg alle som gjelder",
"ui.question.singleHint": "Velg ett svar",
+2
View File
@@ -171,7 +171,9 @@ export const dict = {
"ui.patch.action.patched": "Załatano",
"ui.question.subtitle.answered": "{{count}} odpowiedzi",
"ui.question.subtitle.dismissed": "{{count}} dismissed", // kilocode_change
"ui.question.answer.none": "(brak odpowiedzi)",
"ui.question.answer.dismissed": "Dismissed", // kilocode_change
"ui.question.review.notAnswered": "(bez odpowiedzi)",
"ui.question.multiHint": "Zaznacz wszystkie pasujące",
"ui.question.singleHint": "Wybierz jedną odpowiedź",
+2
View File
@@ -171,7 +171,9 @@ export const dict = {
"ui.patch.action.patched": "Изменено",
"ui.question.subtitle.answered": "{{count}} отвечено",
"ui.question.subtitle.dismissed": "{{count}} dismissed", // kilocode_change
"ui.question.answer.none": "(нет ответа)",
"ui.question.answer.dismissed": "Dismissed", // kilocode_change
"ui.question.review.notAnswered": "(не отвечено)",
"ui.question.multiHint": "Выберите все подходящие",
"ui.question.singleHint": "Выберите один ответ",
+2
View File
@@ -173,7 +173,9 @@ export const dict = {
"ui.patch.action.patched": "แพตช์",
"ui.question.subtitle.answered": "{{count}} ตอบแล้ว",
"ui.question.subtitle.dismissed": "{{count}} dismissed", // kilocode_change
"ui.question.answer.none": "(ไม่มีคำตอบ)",
"ui.question.answer.dismissed": "Dismissed", // kilocode_change
"ui.question.review.notAnswered": "(ไม่ได้ตอบ)",
"ui.question.multiHint": "เลือกทั้งหมดที่ใช้",
"ui.question.singleHint": "เลือกหนึ่งคำตอบ",
+2
View File
@@ -178,7 +178,9 @@ export const dict = {
"ui.patch.action.patched": "Yamalandı",
"ui.question.subtitle.answered": "{{count}} cevaplandı",
"ui.question.subtitle.dismissed": "{{count}} dismissed", // kilocode_change
"ui.question.answer.none": "(cevap yok)",
"ui.question.answer.dismissed": "Dismissed", // kilocode_change
"ui.question.review.notAnswered": "(cevaplanmadı)",
"ui.question.multiHint": "Geçerli tüm cevapları seçin",
"ui.question.singleHint": "Bir cevap seçin",
+2
View File
@@ -198,7 +198,9 @@ export const dict: Record<string, string> = {
"ui.patch.action.patched": "Застосовано патч", // kilocode_change
"ui.question.subtitle.answered": "{{count}} відповідей",
"ui.question.subtitle.dismissed": "{{count}} dismissed", // kilocode_change
"ui.question.answer.none": "(немає відповіді)",
"ui.question.answer.dismissed": "Dismissed", // kilocode_change
"ui.question.review.notAnswered": "(не відповіли)",
"ui.question.multiHint": "Виберіть усі відповідні варіанти",
"ui.question.singleHint": "Виберіть одну відповідь",
+2
View File
@@ -175,7 +175,9 @@ export const dict = {
"ui.patch.action.patched": "已应用补丁",
"ui.question.subtitle.answered": "{{count}} 已回答",
"ui.question.subtitle.dismissed": "{{count}} dismissed", // kilocode_change
"ui.question.answer.none": "(无答案)",
"ui.question.answer.dismissed": "Dismissed", // kilocode_change
"ui.question.review.notAnswered": "(未回答)",
"ui.question.multiHint": "可多选",
"ui.question.singleHint": "选择一个答案",
+2
View File
@@ -175,7 +175,9 @@ export const dict = {
"ui.patch.action.patched": "已套用修補",
"ui.question.subtitle.answered": "{{count}} 已回答",
"ui.question.subtitle.dismissed": "{{count}} dismissed", // kilocode_change
"ui.question.answer.none": "(無答案)",
"ui.question.answer.dismissed": "Dismissed", // kilocode_change
"ui.question.review.notAnswered": "(未回答)",
"ui.question.multiHint": "可多選",
"ui.question.singleHint": "選擇一個答案",