Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion scripts/test_nebula_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def test_banner_does_not_repeat_question() -> None:
js = (
ROOT / "src" / "ask_question_mcp" / "assets" / "dialog" / "dialog.js"
).read_text(encoding="utf-8")
assert 'banner-copy").textContent' in js or "$(\"#banner-copy\")" in js
assert 'banner-copy").textContent' in js or '$("#banner-copy")' in js
# Old bug: `${prefix}${payload.question || ""}`
assert "${prefix}${payload.question" not in js, (
"REGRESSION: banner-copy must not append payload.question "
Expand All @@ -58,7 +58,21 @@ def test_banner_does_not_repeat_question() -> None:
assert "prefix}${payload.question" not in js


def test_nebula_lead_detail_split() -> None:
"""Nebula must split dense Preloop ' · ' packs like Gtk (lead + detail)."""
html = INDEX.read_text(encoding="utf-8")
js = (
ROOT / "src" / "ask_question_mcp" / "assets" / "dialog" / "dialog.js"
).read_text(encoding="utf-8")
assert 'id="question-detail"' in html, "missing #question-detail"
assert "function formatConfirmBody" in js
assert "function splitLeadDetail" in js
assert "function renderQuestion" in js
assert "renderQuestion(payload.question" in js


if __name__ == "__main__":
test_freeform_inside_main_body()
test_banner_does_not_repeat_question()
test_nebula_lead_detail_split()
print("test_nebula_layout: ok")
31 changes: 31 additions & 0 deletions src/ask_question_mcp/assets/dialog/dialog.css
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,37 @@ body {
transform: translateY(0);
}

.question-block {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 0;
}

.question-detail {
margin: 0;
max-height: 160px;
overflow-x: hidden;
overflow-y: auto;
font-size: 13.5px;
font-weight: 450;
letter-spacing: -0.01em;
line-height: 1.45;
color: var(--text-muted, color-mix(in srgb, var(--text) 72%, transparent));
white-space: pre-wrap;
opacity: 0;
transform: translateY(8px);
transition:
opacity 720ms var(--ease),
transform 720ms var(--ease);
transition-delay: 140ms;
}

.app.is-ready .question-detail {
opacity: 1;
transform: translateY(0);
}

[data-theme="ink"] .question,
[data-theme="hybrid"] .question {
font-weight: 520;
Expand Down
52 changes: 50 additions & 2 deletions src/ask_question_mcp/assets/dialog/dialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,52 @@
return String(label || "").replace(/^\d+\s*[·.]\s*/, "").trim();
}

/** Match dialog_keys.format_confirm_body — dense " · " packs become lines. */
function formatConfirmBody(question) {
const q = String(question || "").trim();
if (!q) return q;
if (q.includes("\n")) return q;
if (q.includes(" · ")) {
const parts = q
.split(" · ")
.map((p) => p.trim())
.filter(Boolean);
if (parts.length >= 2) return parts.join("\n");
}
return q;
}

/** Match dialog_keys.split_lead_detail — first line = ask/referent lead. */
function splitLeadDetail(body) {
const text = String(body || "").replace(/^\n+|\n+$/g, "");
if (!text.trim()) return ["", ""];
const lines = text.split("\n");
let i = 0;
while (i < lines.length && !lines[i].trim()) i += 1;
if (i >= lines.length) return ["", ""];
const lead = lines[i];
const rest = lines.slice(i + 1);
while (rest.length && !rest[0].trim()) rest.shift();
return [lead, rest.join("\n").replace(/\n+$/g, "")];
}

function renderQuestion(rawQuestion) {
const body = formatConfirmBody(rawQuestion);
const [lead, detail] = splitLeadDetail(body);
const qEl = $("#question");
const dEl = $("#question-detail");
if (qEl) qEl.textContent = lead || body || "";
if (dEl) {
if (detail) {
dEl.hidden = false;
dEl.textContent = detail;
} else {
dEl.hidden = true;
dEl.textContent = "";
}
}
}

function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
Expand Down Expand Up @@ -690,7 +736,7 @@
if (bandClass) eyebrowEl.classList.add(bandClass);
else if (dangerous) eyebrowEl.classList.add("is-danger");
$("#title-agent").textContent = payload.agent_hint || payload.title || "";
$("#question").textContent = payload.question || "";
renderQuestion(payload.question || "");

const banner = $("#banner");
banner.classList.toggle("is-on", dangerous);
Expand Down Expand Up @@ -804,14 +850,16 @@
function fitWindow() {
const chrome = document.querySelector(".chrome");
const banner = document.getElementById("banner");
const questionBlock = document.getElementById("question-block");
const question = document.getElementById("question");
const options = document.getElementById("options");
const refs = document.getElementById("refs");
const freeform = document.getElementById("freeform");
const footer = document.querySelector(".footer");
// Titlebar + breathing room; too-tight budgets clip OK/Cancel on scaled monitors.
let h = 36;
[chrome, question, refs, freeform, footer].forEach((el) => {
const qMeasure = questionBlock || question;
[chrome, qMeasure, refs, freeform, footer].forEach((el) => {
if (el && !el.hidden) h += el.offsetHeight;
});
// Voice recover chrome can grow after mount — keep Cancel/OK on-screen.
Expand Down
5 changes: 4 additions & 1 deletion src/ask_question_mcp/assets/dialog/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@
<div class="banner-copy" id="banner-copy"></div>
</div>
</div>
<h1 class="question" id="question"></h1>
<div class="question-block" id="question-block">
<h1 class="question" id="question"></h1>
<div class="question-detail" id="question-detail" hidden></div>
</div>
<div class="options" id="options" role="listbox"></div>

<!-- Linux WebKit/Adw: refs + freeform must live in the scrolling
Expand Down