diff --git a/crates/giskard-server/static/app.css b/crates/giskard-server/static/app.css index 50da83d..43ff0f6 100644 --- a/crates/giskard-server/static/app.css +++ b/crates/giskard-server/static/app.css @@ -96,7 +96,7 @@ .project-toggle { width:24px; height:24px; padding:0; border-color:transparent; background:transparent; color:var(--muted); line-height:1; } .project-toggle:hover { border-color:var(--border); color:var(--fg); } .project-name { - flex:1; min-width:0; padding:2px 0; border:0; background:transparent; color:var(--fg); + flex:1; min-width:0; padding:5px 8px; border:0; border-radius:6px; background:transparent; color:var(--fg); font-weight:600; text-align:left; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .project-name:hover { border-color:transparent; color:var(--accent); } @@ -116,7 +116,8 @@ /* Selected thread: a whisper of accent tint over the neutral raised fill plus a thin accent rail, no bold. The ~9% tint gives selection a hint of color that a plain hover (bare --panel2) lacks, so the two separate cleanly at a glance while staying restrained and editor-like. */ - .thread.active { background:var(--panel2); background:color-mix(in srgb, var(--accent) 9%, var(--panel2)); color:var(--fg); box-shadow:inset 2px 0 0 var(--accent); } + .thread.active, + .project-name.active { background:var(--panel2); background:color-mix(in srgb, var(--accent) 9%, var(--panel2)); color:var(--fg); box-shadow:inset 2px 0 0 var(--accent); } .thread.active .thread-title { color:var(--fg); } .thread-status { flex:none; width:10px; text-align:center; color:var(--muted); font-weight:700; } .thread-title { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } @@ -136,7 +137,8 @@ the only persistent signal on touch. */ @media (hover: hover) { .thread:hover { background:var(--panel2); } - .thread.active:hover { background:var(--panel2); background:color-mix(in srgb, var(--accent) 13%, var(--panel2)); } + .thread.active:hover, + .project-name.active:hover { background:var(--panel2); background:color-mix(in srgb, var(--accent) 13%, var(--panel2)); color:var(--fg); } .thread-menu-btn:hover { border-color:var(--border); color:var(--fg); } } .thread-menu { position:absolute; right:0; top:30px; z-index:20; min-width:130px; padding:4px; border:1px solid var(--border); border-radius:6px; background:var(--panel); box-shadow:0 12px 32px rgba(0,0,0,.35); } diff --git a/crates/giskard-server/static/app.js b/crates/giskard-server/static/app.js index 73aef3c..741e4bd 100644 --- a/crates/giskard-server/static/app.js +++ b/crates/giskard-server/static/app.js @@ -1003,7 +1003,7 @@ function threadRow(pid, t) { const title = t.title || t.id.slice(0,8); const el = document.createElement("div"); el.className="thread mono"; applyThreadTitleToElement(el, pid, t.id, title); - markThreadRowActive(el, !!state.threadId && String(t.id) === String(state.threadId)); + markSidebarRowActive(el, !!state.threadId && String(t.id) === String(state.threadId)); const menuBtn = document.createElement("button"); menuBtn.type = "button"; menuBtn.className = "thread-menu-btn"; @@ -1364,23 +1364,28 @@ function renderAllThreadActivityIndicators() { document.querySelectorAll(".thread").forEach(el => renderThreadActivityIndicator(el.dataset.tid, hosts)); } -// Mark a single thread row as the selected one (or not). `aria-current` mirrors the visual state for +// Mark a sidebar row as the selected one (or not). `aria-current` mirrors the visual state for // assistive tech and gives the CSS a stable hook. -function markThreadRowActive(el, active) { +function markSidebarRowActive(el, active) { if (!el) return; el.classList.toggle("active", active); if (active) el.setAttribute("aria-current", "true"); else el.removeAttribute("aria-current"); } -// Derive the sidebar selection from `state.threadId` rather than tracking it imperatively. Thread -// rows are rebuilt whenever the list reloads, so any highlight set by hand goes stale on the next -// re-render (leaving a previously selected row looking selected); recomputing from the single source -// of truth keeps exactly one row active. +// Derive the sidebar selection from thread/draft state rather than tracking it imperatively. +// Rows are rebuilt whenever the list reloads, so any highlight set by hand goes stale on the next +// re-render; recomputing from the single source of truth keeps persisted threads and draft project +// rows mutually exclusive. function syncActiveThreadHighlight() { const tid = state.threadId ? String(state.threadId) : null; + const draftPid = isDraftThread() && state.projectId ? String(state.projectId) : null; document.querySelectorAll(".thread").forEach(el => - markThreadRowActive(el, tid !== null && String(el.dataset.tid) === tid)); + markSidebarRowActive(el, tid !== null && String(el.dataset.tid) === tid)); + document.querySelectorAll(".project-name").forEach(el => { + const project = el.closest(".proj"); + markSidebarRowActive(el, draftPid !== null && project && String(project.dataset.pid) === draftPid); + }); } function setThreadActivity(tid, activity) { diff --git a/docs/screenshots/ide-desktop.png b/docs/screenshots/ide-desktop.png index fed483a..d21f737 100644 Binary files a/docs/screenshots/ide-desktop.png and b/docs/screenshots/ide-desktop.png differ diff --git a/tests/e2e/tests/draft-composer.spec.ts b/tests/e2e/tests/draft-composer.spec.ts index 155c650..78ae603 100644 --- a/tests/e2e/tests/draft-composer.spec.ts +++ b/tests/e2e/tests/draft-composer.spec.ts @@ -48,6 +48,30 @@ test.describe("draft composer", () => { const modelButton = (page: Page) => page.locator("#modelPickerBtn"); + test("highlights the project that owns the current draft", async ({ page }) => { + const project = page.locator(".proj", { hasText: "Demo" }); + const projectName = project.locator(".project-name"); + + await project.locator(".project-add").click(); + + await expect(page.locator("#transcript")).toContainText("Start a new thread"); + await expect(projectName).toHaveClass(/\bactive\b/); + await expect(projectName).toHaveAttribute("aria-current", "true"); + await expect(page.locator(".thread.active")).toHaveCount(0); + + const shadow = await projectName.evaluate((el) => getComputedStyle(el).boxShadow); + expect(shadow).not.toBe("none"); + + await page.locator("#input").fill("Create the highlighted draft's thread"); + await page.locator("#sendBtn").click(); + await expect( + page.locator("#transcript .msg.agent", { hasText: SCRIPTED_REPLY }), + ).toBeVisible(); + + await expect(projectName).not.toHaveClass(/\bactive\b/); + await expect(page.locator(".thread.active")).toHaveCount(1); + }); + test("keeps a message typed while the new thread is still opening", async ({ page }) => { const message = "Typed before the draft finished opening"; const release = await holdProjectFetch(page);