Merge pull request #6660 from Kilo-Org/fix/6623-question-prompt-formatting

This commit is contained in:
Igor Šćekić
2026-03-05 14:20:13 +01:00
committed by GitHub
8 changed files with 207 additions and 315 deletions
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:550464711e0d3aab99fa514d6de9501be19e49b65d5643b7762fb7b5ca47464c
size 19715
oid sha256:b8b1ba56d6632bfcbd0e4c5b0c1514cd80832339dde0c86568572ada656ca33d
size 24134
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2cdb43fe23e17f32dd96b36eb3b2f97b4b84c29983996f26406db6ead2f41785
size 25707
oid sha256:278855516fe11d95b9de942d9690531c114b0a0b9ac0239c2ef31f82b420bd7e
size 32467
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:18bd63c206b2afc5d8d05e06a8a1113c7dfa1019e85eebe570be40419dbe35fd
size 28187
oid sha256:f6193ba79b4d5dda29bf39d1127dacd7e332755d1f6f42dd67ff667088975659
size 35678
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:607707e2201d1e50369a5897266136387b2444570b61ac2062c658335a0cdcc7
size 5055
@@ -1,12 +1,13 @@
/**
* QuestionDock component
* Displays question requests from the AI assistant inline above the prompt input.
* Uses kilo-ui's data-component/data-slot CSS pattern for styling.
* Uses kilo-ui's DockPrompt component for proper surface styling.
*/
import { Component, For, Show, createMemo, createEffect } from "solid-js"
import { createStore } from "solid-js/store"
import { Button } from "@kilocode/kilo-ui/button"
import { DockPrompt } from "@kilocode/kilo-ui/dock-prompt"
import { Icon } from "@kilocode/kilo-ui/icon"
import { useSession } from "../../context/session"
import { useLanguage } from "../../context/language"
@@ -148,10 +149,11 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
}
return (
<div data-component="tool-part-wrapper" data-question="true" onClick={(e: MouseEvent) => e.stopPropagation()}>
<div data-component="question-prompt">
<div data-slot="question-body">
<div data-slot="question-header">
<div onClick={(e: MouseEvent) => e.stopPropagation()}>
<DockPrompt
kind="question"
header={
<>
<div data-slot="question-header-title">{summary()}</div>
<Show when={!single()}>
<div data-slot="question-progress">
@@ -177,62 +179,65 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
</button>
</div>
</Show>
</div>
<Show when={!confirm()}>
<div data-slot="question-content">
<div data-slot="question-text">{question()?.question}</div>
<Show
when={multi()}
fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}
>
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
</>
}
footer={
<>
<Button variant="ghost" size="small" onClick={reject} disabled={store.sending}>
{language.t("ui.common.dismiss")}
</Button>
<div data-slot="question-footer-actions">
<Show when={store.tab > 0}>
<Button variant="secondary" size="small" onClick={back} disabled={store.sending}>
{language.t("ui.common.back")}
</Button>
</Show>
<div data-slot="question-options">
<For each={options()}>
{(opt, i) => {
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
return (
<button
data-slot="question-option"
data-picked={picked()}
disabled={store.sending}
onClick={() => selectOption(i())}
>
<span data-slot="question-option-check" aria-hidden="true">
<span
data-slot="question-option-box"
data-type={multi() ? "checkbox" : "radio"}
data-picked={picked()}
>
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
<Icon name="check-small" size="small" />
</Show>
</span>
</span>
<span data-slot="question-option-main">
<span data-slot="option-label">{opt.label}</span>
<Show when={opt.description}>
<span data-slot="option-description">{opt.description}</span>
</Show>
</span>
</button>
)
}}
</For>
<Show when={question()?.custom !== false}>
<Show
when={confirm()}
fallback={
<Button
variant={last() && single() ? "primary" : "secondary"}
size="small"
onClick={last() && single() ? submit : () => selectTab(store.tab + 1)}
disabled={store.sending || (!confirm() && (store.answers[store.tab]?.length ?? 0) === 0)}
>
{last() && single()
? language.t("ui.common.submit")
: last()
? language.t("common.review")
: language.t("ui.common.next")}
</Button>
}
>
<Button variant="primary" size="small" onClick={submit} disabled={store.sending}>
{language.t("ui.common.submit")}
</Button>
</Show>
</div>
</>
}
>
<Show when={!confirm()}>
<div data-slot="question-text">{question()?.question}</div>
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
</Show>
<div data-slot="question-options">
<For each={options()}>
{(opt, i) => {
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
return (
<button
data-slot="question-option"
data-custom="true"
data-picked={customPicked()}
data-picked={picked()}
disabled={store.sending}
onClick={() => selectOption(options().length)}
onClick={() => selectOption(i())}
>
<span data-slot="question-option-check" aria-hidden="true">
<span
data-slot="question-option-box"
data-type={multi() ? "checkbox" : "radio"}
data-picked={customPicked()}
data-picked={picked()}
>
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
<Icon name="check-small" size="small" />
@@ -240,101 +245,94 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
</span>
</span>
<span data-slot="question-option-main">
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
<span data-slot="option-description" data-placeholder={!input()}>
{input() || language.t("ui.question.custom.placeholder")}
</span>
<span data-slot="option-label">{opt.label}</span>
<Show when={opt.description}>
<span data-slot="option-description">{opt.description}</span>
</Show>
</span>
</button>
<Show when={store.editing}>
<form data-slot="custom-input-form" onSubmit={handleCustomSubmit}>
<input
ref={(el) => setTimeout(() => el.focus(), 0)}
type="text"
data-slot="custom-input"
placeholder={language.t("ui.question.custom.placeholder")}
value={input()}
disabled={store.sending}
onInput={(e) => {
const inputs = [...store.custom]
inputs[store.tab] = e.currentTarget.value
setStore("custom", inputs)
}}
/>
<Button type="submit" variant="primary" size="small" disabled={store.sending}>
{multi() ? language.t("ui.common.add") : language.t("ui.common.submit")}
</Button>
<Button
type="button"
variant="ghost"
size="small"
disabled={store.sending}
onClick={() => setStore("editing", false)}
>
{language.t("ui.common.cancel")}
</Button>
</form>
</Show>
</Show>
</div>
</div>
</Show>
<Show when={confirm()}>
<div data-slot="question-review">
<div data-slot="review-title">{language.t("ui.messagePart.review.title")}</div>
<For each={questions()}>
{(q, index) => {
const value = () => store.answers[index()]?.join(", ") ?? ""
const answered = () => Boolean(value())
return (
<div data-slot="review-item">
<span data-slot="review-label">{q.question}</span>
<span data-slot="review-value" data-answered={answered()}>
{answered() ? value() : language.t("ui.question.review.notAnswered")}
</span>
</div>
)
}}
</For>
</div>
</Show>
</div>
<div data-slot="question-footer">
<Button variant="ghost" size="small" onClick={reject} disabled={store.sending}>
{language.t("ui.common.dismiss")}
</Button>
<div data-slot="question-footer-actions">
<Show when={store.tab > 0}>
<Button variant="secondary" size="small" onClick={back} disabled={store.sending}>
{language.t("ui.common.back")}
</Button>
</Show>
<Show
when={confirm()}
fallback={
<Button
variant={last() && single() ? "primary" : "secondary"}
size="small"
onClick={last() && single() ? submit : () => selectTab(store.tab + 1)}
disabled={store.sending || (!confirm() && (store.answers[store.tab]?.length ?? 0) === 0)}
>
{last() && single()
? language.t("ui.common.submit")
: last()
? language.t("common.review")
: language.t("ui.common.next")}
</Button>
}
>
<Button variant="primary" size="small" onClick={submit} disabled={store.sending}>
{language.t("ui.common.submit")}
</Button>
)
}}
</For>
<Show when={question()?.custom !== false}>
<button
data-slot="question-option"
data-custom="true"
data-picked={customPicked()}
disabled={store.sending}
onClick={() => selectOption(options().length)}
>
<span data-slot="question-option-check" aria-hidden="true">
<span
data-slot="question-option-box"
data-type={multi() ? "checkbox" : "radio"}
data-picked={customPicked()}
>
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
<Icon name="check-small" size="small" />
</Show>
</span>
</span>
<span data-slot="question-option-main">
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
<span data-slot="option-description" data-placeholder={!input()}>
{input() || language.t("ui.question.custom.placeholder")}
</span>
</span>
</button>
<Show when={store.editing}>
<form data-slot="custom-input-form" onSubmit={handleCustomSubmit}>
<input
ref={(el) => setTimeout(() => el.focus(), 0)}
type="text"
data-slot="custom-input"
placeholder={language.t("ui.question.custom.placeholder")}
value={input()}
disabled={store.sending}
onInput={(e) => {
const inputs = [...store.custom]
inputs[store.tab] = e.currentTarget.value
setStore("custom", inputs)
}}
/>
<Button type="submit" variant="primary" size="small" disabled={store.sending}>
{multi() ? language.t("ui.common.add") : language.t("ui.common.submit")}
</Button>
<Button
type="button"
variant="ghost"
size="small"
disabled={store.sending}
onClick={() => setStore("editing", false)}
>
{language.t("ui.common.cancel")}
</Button>
</form>
</Show>
</Show>
</div>
</div>
</div>
</Show>
<Show when={confirm()}>
<div data-slot="question-review">
<div data-slot="review-title">{language.t("ui.messagePart.review.title")}</div>
<For each={questions()}>
{(q, index) => {
const value = () => store.answers[index()]?.join(", ") ?? ""
const answered = () => Boolean(value())
return (
<div data-slot="review-item">
<span data-slot="review-label">{q.question}</span>
<span data-slot="review-value" data-answered={answered()}>
{answered() ? value() : language.t("ui.question.review.notAnswered")}
</span>
</div>
)
}}
</For>
</div>
</Show>
</DockPrompt>
</div>
)
}
@@ -231,6 +231,23 @@ const questionToolPart: ToolPart = {
},
}
const questionDismissedPart: ToolPart = {
id: "part-question-dismissed-001",
sessionID: SESSION_ID,
messageID: ASST_MSG_ID,
type: "tool",
callID: "call-question-dismissed-001",
tool: "question",
state: {
status: "error",
input: { question: "Which testing framework?", options: [] },
error: "Error: User dismissed this question",
title: "Question dismissed",
metadata: {},
time: { start: now - 2000, end: now - 1500 },
},
}
// ---------------------------------------------------------------------------
// Data helpers
// ---------------------------------------------------------------------------
@@ -457,3 +474,19 @@ export const InlineQuestion: Story = {
)
},
}
// ---------------------------------------------------------------------------
// 9. Dismissed question (right-aligned "Questions dismissed" text)
// ---------------------------------------------------------------------------
export const QuestionDismissed: Story = {
name: "Question Dismissed",
render: () => {
const data = dataWith([textPart, questionDismissedPart])
return (
<StoryProviders data={data} sessionID={SESSION_ID}>
<AssistantMessage message={baseAssistantMessage} />
</StoryProviders>
)
},
}
@@ -912,6 +912,10 @@
padding: 0 4px;
}
.vscode-session-turn + .vscode-session-turn {
margin-top: 12px;
}
.vscode-session-turn-user {
width: 100%;
}
@@ -937,14 +941,10 @@
}
/* ============================================
Question Prompt
Question Prompt (VS Code overrides)
============================================ */
[data-component="question-prompt"] {
[data-slot="question-header"] {
padding: 0;
}
[data-component="dock-prompt"][data-kind="question"] {
[data-slot="question-progress-nav"] {
display: inline-flex;
align-items: center;
@@ -972,12 +972,6 @@
}
}
[data-slot="question-footer"] {
padding: 8px;
padding-top: 8px;
margin-top: 0;
}
[data-slot="question-footer"] > [data-component="button"][data-variant="ghost"] {
border: none;
box-shadow: none;
@@ -997,155 +991,11 @@
color: var(--text-weaker);
}
[data-slot="question-text"] {
font-size: var(--font-size-base);
padding: 0 2px;
}
[data-slot="question-hint"] {
font-size: var(--font-size-small);
padding: 0 2px;
}
[data-slot="question-options"] {
margin-top: 8px;
gap: 4px;
padding: 1px 1px 4px;
}
[data-slot="question-option"] {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 6px 8px;
background-color: var(--surface-raised-stronger-non-alpha);
border: 1px solid var(--border-weak-base);
border-radius: var(--radius-lg);
box-shadow: none;
text-align: left;
width: 100%;
cursor: pointer;
transition:
background-color 0.15s ease,
border-color 0.15s ease,
box-shadow 0.15s ease;
&:hover:not([data-picked="true"]) {
background-color: var(--background-base);
}
&[data-picked="true"] {
background-color: var(--surface-interactive-weak);
border-color: transparent;
box-shadow: var(--shadow-xs-border-hover);
}
&:disabled {
cursor: not-allowed;
opacity: 0.6;
}
}
[data-slot="question-option-check"] {
display: inline-flex;
transform: translateY(2px);
}
[data-slot="question-option-box"] {
width: 16px;
height: 16px;
padding: 2px;
border-radius: var(--radius-sm);
border: 1px solid var(--border-weak-base);
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background-color: transparent;
transition:
background-color 0.15s ease,
border-color 0.15s ease;
[data-component="icon"] {
opacity: 0;
color: var(--icon-base);
}
&[data-type="radio"] {
border-radius: 999px;
}
[data-slot="question-option-radio-dot"] {
width: 6px;
height: 6px;
border-radius: 999px;
background-color: var(--background-stronger);
opacity: 0;
}
&[data-picked="true"] {
border-color: var(--icon-interactive-base);
[data-component="icon"] {
opacity: 1;
color: var(--icon-invert-base);
}
&[data-type="checkbox"] {
background-color: var(--icon-interactive-base);
}
&[data-type="radio"] {
background-color: var(--icon-interactive-base);
[data-slot="question-option-radio-dot"] {
opacity: 1;
}
}
}
}
[data-slot="question-option-main"] {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
flex: 1;
}
[data-slot="option-label"] {
font-family: var(--font-family-sans);
font-size: var(--font-size-base);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large);
color: var(--text-strong);
}
[data-slot="option-description"] {
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
font-weight: var(--font-weight-regular);
line-height: var(--line-height-large);
color: var(--text-base);
min-width: 0;
white-space: normal;
overflow: visible;
text-overflow: clip;
}
[data-slot="question-option"][data-custom="true"] {
[data-slot="option-description"] {
overflow: visible;
text-overflow: clip;
white-space: normal;
overflow-wrap: anywhere;
}
}
[data-slot="question-review"] {
display: flex;
flex-direction: column;
gap: 8px;
padding: 4px 2px;
padding: 4px 10px;
}
[data-slot="review-title"] {
+17 -9
View File
@@ -964,22 +964,29 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
const render = ToolRegistry.render(part.tool) ?? GenericTool
const dismissed = createMemo(
() =>
part.tool === "question" &&
part.state.status === "error" &&
typeof part.state.error === "string" &&
part.state.error.includes("dismissed this question"),
)
return (
<Show when={!hideQuestion()}>
<Show when={dismissed()}>
<div style="width: 100%; display: flex; justify-content: flex-end; padding: 4px 8px 4px 0;">
<span class="text-13-regular text-text-weak cursor-default">
{i18n.t("ui.tool.questions")} dismissed
</span>
</div>
</Show>
<Show when={!dismissed()}>
<div data-component="tool-part-wrapper">
<Switch>
<Match when={part.state.status === "error" && part.state.error}>
{(error) => {
const cleaned = error().replace("Error: ", "")
if (part.tool === "question" && cleaned.includes("dismissed this question")) {
return (
<div style="width: 100%; display: flex; justify-content: flex-end;">
<span class="text-13-regular text-text-weak cursor-default">
{i18n.t("ui.tool.questions")} dismissed
</span>
</div>
)
}
const [title, ...rest] = cleaned.split(": ")
return (
<Card variant="error">
@@ -1016,6 +1023,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
</Match>
</Switch>
</div>
</Show>
</Show>
)
}