polish: refine sidebar activity indicators, add placeholder token, and tidy search field (#8606)
Some checks are pending
Canary / Prepare Version (push) Waiting to run
Canary / build-cli (push) Blocked by required conditions
Canary / Upload Install Script (push) Blocked by required conditions
Canary / bundle-desktop (push) Blocked by required conditions
Canary / bundle-desktop-intel (push) Blocked by required conditions
Canary / bundle-desktop-linux (push) Blocked by required conditions
Canary / bundle-desktop-windows (push) Blocked by required conditions
Canary / Release (push) Blocked by required conditions
Cargo Deny / deny (push) Waiting to run
Unused Dependencies / machete (push) Waiting to run
CI / changes (push) Waiting to run
CI / Check Rust Code Format (push) Blocked by required conditions
CI / Build and Test Rust Project (push) Blocked by required conditions
CI / Build Rust Project on Windows (push) Waiting to run
CI / Lint Rust Code (push) Blocked by required conditions
CI / Check Generated Schemas are Up-to-Date (push) Blocked by required conditions
CI / Test and Lint Electron Desktop App (push) Blocked by required conditions
Deploy Documentation / deploy (push) Waiting to run
Goose 2 CI / Lint & Format (push) Waiting to run
Goose 2 CI / Unit Tests (push) Waiting to run
Goose 2 CI / Desktop Build & E2E (push) Waiting to run
Goose 2 CI / Rust Lint (push) Waiting to run
Live Provider Tests / Smoke Tests (Code Execution) (push) Blocked by required conditions
Live Provider Tests / check-fork (push) Waiting to run
Live Provider Tests / changes (push) Blocked by required conditions
Live Provider Tests / Build Binary (push) Blocked by required conditions
Live Provider Tests / Smoke Tests (push) Blocked by required conditions
Live Provider Tests / Compaction Tests (push) Blocked by required conditions
Live Provider Tests / goose server HTTP integration tests (push) Blocked by required conditions
Publish Ask AI Bot Docker Image / docker (push) Waiting to run
Publish Docker Image / docker (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
This commit is contained in:
morgmart 2026-04-16 12:28:32 -07:00 committed by GitHub
parent fdd8ada032
commit bd14186214
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 95 additions and 57 deletions

View file

@ -315,7 +315,7 @@ export function Sidebar({
collapsed ? "justify-center" : "justify-between",
)}
>
<GooseIcon className="text-muted-foreground" />
<GooseIcon className="text-foreground" />
{!collapsed && (
<Button
type="button"
@ -367,49 +367,36 @@ export function Sidebar({
<div
className={cn(
"flex items-center w-full rounded-md transition-all duration-300 ease-out",
"mb-4 flex items-center w-full rounded-md transition-all duration-300 ease-out",
collapsed
? "justify-center p-3 text-muted-foreground"
: "gap-2 border border-border px-2.5 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-transparent",
)}
>
<Search className="size-3.5 flex-shrink-0" />
<Search className="size-3.5 flex-shrink-0 text-placeholder" />
{!collapsed && (
<>
<input
ref={searchInputRef}
type="text"
enterKeyHint="search"
value={sidebarSearch.query}
onChange={(e) => sidebarSearch.setQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
void sidebarSearch.search();
}
}}
placeholder={t("search.placeholder")}
className={cn(
"focus-override appearance-none bg-transparent border-none text-xs flex-1 min-w-0 placeholder:text-muted-foreground outline-none focus-visible:ring-0 focus-visible:ring-offset-0",
labelTransition,
labelVisible
? "opacity-100 w-auto"
: "opacity-0 w-0 overflow-hidden",
)}
onClick={(e) => e.stopPropagation()}
/>
<kbd
className={cn(
"text-[10px] text-muted-foreground px-1 py-0.5 rounded font-mono flex-shrink-0",
labelTransition,
labelVisible
? "opacity-100 w-auto"
: "opacity-0 w-0 overflow-hidden px-0",
)}
>
K
</kbd>
</>
<input
ref={searchInputRef}
type="text"
enterKeyHint="search"
value={sidebarSearch.query}
onChange={(e) => sidebarSearch.setQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
void sidebarSearch.search();
}
}}
placeholder={t("search.placeholder")}
className={cn(
"focus-override appearance-none bg-transparent border-none text-xs flex-1 min-w-0 placeholder:text-placeholder outline-none focus-visible:ring-0 focus-visible:ring-offset-0",
labelTransition,
labelVisible
? "opacity-100 w-auto"
: "opacity-0 w-0 overflow-hidden",
)}
onClick={(e) => e.stopPropagation()}
/>
)}
</div>

View file

@ -64,6 +64,7 @@ export function SidebarChatRow({
t("common:session.defaultTitle"),
);
const [draftTitle, setDraftTitle] = useState(editableTitle);
const showActivityIndicator = isRunning || hasUnread;
useEffect(() => {
setDraftTitle(editableTitle);
@ -186,10 +187,17 @@ export function SidebarChatRow({
isActive ? ACTIVE_CHAT_ROW_CLASS : INACTIVE_CHAT_ROW_CLASS,
)}
>
{showActivityIndicator && (
<span className="flex h-3 w-3 shrink-0 items-center justify-center">
<SessionActivityIndicator
isRunning={isRunning}
hasUnread={hasUnread}
/>
</span>
)}
<span className="flex-1 min-w-0 truncate text-left">
{displayTitle}
</span>
<SessionActivityIndicator isRunning={isRunning} hasUnread={hasUnread} />
</Button>
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>

View file

@ -108,6 +108,37 @@ describe("SidebarChatRow", () => {
expect(screen.getByLabelText(/unread messages/i)).toBeInTheDocument();
});
it("does not reserve activity space by default when idle", () => {
const { container } = render(
<SidebarChatRow id="session-1" title="Idle Chat" isActive={false} />,
);
expect(
container.querySelector(".h-3.w-3.shrink-0.items-center.justify-center"),
).toBeNull();
});
it("reserves activity space only once activity exists", () => {
const { container, rerender } = render(
<SidebarChatRow id="session-1" title="Recent Chat" isActive={false} />,
);
expect(
container.querySelector(".h-3.w-3.shrink-0.items-center.justify-center"),
).toBeNull();
rerender(
<SidebarChatRow
id="session-1"
title="Recent Chat"
isActive={false}
hasUnread
/>,
);
expect(screen.getByLabelText(/unread messages/i)).toBeInTheDocument();
});
it("keeps the localized default title in rename mode without persisting it", async () => {
const user = userEvent.setup();
const onRename = vi.fn();

View file

@ -122,6 +122,7 @@
--text-success: var(--color-green-300);
--text-warning: var(--color-yellow-200);
--text-info: var(--color-blue-200);
--text-placeholder: var(--color-gray-400);
--ring: color-mix(in srgb, var(--border-strong) 20%, transparent);
@ -304,6 +305,7 @@
--text-success: var(--color-green-100);
--text-warning: var(--color-yellow-100);
--text-info: var(--color-blue-100);
--text-placeholder: var(--color-gray-600);
--ring: color-mix(in srgb, var(--border-strong) 20%, transparent);
@ -356,6 +358,7 @@
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-placeholder: var(--text-placeholder);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
@ -407,6 +410,7 @@
--color-text-success: var(--text-success);
--color-text-warning: var(--text-warning);
--color-text-info: var(--text-info);
--color-text-placeholder: var(--text-placeholder);
/* alpha variants */
--color-dark-10: var(--dark-10);

View file

@ -3,25 +3,22 @@ import { describe, expect, it } from "vitest";
import { SessionActivityIndicator } from "./SessionActivityIndicator";
describe("SessionActivityIndicator", () => {
it("renders a brand-colored inline spinner for running sessions", () => {
it("renders an inline spinner for running sessions", () => {
render(<SessionActivityIndicator isRunning />);
expect(screen.getByLabelText(/chat active/i)).toHaveClass("text-brand");
expect(screen.getByLabelText(/chat active/i)).toBeInTheDocument();
});
it("renders a brand-colored inline dot for unread sessions", () => {
it("renders an inline dot for unread sessions", () => {
render(<SessionActivityIndicator hasUnread />);
expect(screen.getByLabelText(/unread messages/i)).toHaveClass("bg-brand");
expect(screen.getByLabelText(/unread messages/i)).toBeInTheDocument();
});
it("renders an overlay spinner variant for running sessions", () => {
const { container } = render(
<SessionActivityIndicator isRunning variant="overlay" />,
);
render(<SessionActivityIndicator isRunning variant="overlay" />);
expect(screen.getByLabelText(/chat active/i)).toBeInTheDocument();
expect(container.querySelector(".text-brand")).toBeTruthy();
});
it("renders nothing when the session is idle and read", () => {

View file

@ -21,24 +21,32 @@ export function SessionActivityIndicator({
role="status"
aria-label="Chat active"
className={cn(
"absolute -right-1 -top-1 flex h-3.5 w-3.5 items-center justify-center rounded-full border border-background bg-background shadow-sm",
"absolute -right-1 -top-1 flex h-3.5 w-3.5 items-center justify-center rounded-full border border-background bg-background shadow-sm transition-opacity duration-200 ease-out animate-in fade-in-0",
className,
)}
>
<Loader2
aria-hidden="true"
className="h-2.5 w-2.5 animate-spin text-brand"
className="h-2.5 w-2.5 animate-spin text-text-info"
/>
</span>
);
}
return (
<Loader2
<span
role="status"
aria-label="Chat active"
className={cn("h-3 w-3 shrink-0 animate-spin text-brand", className)}
/>
className={cn(
"inline-flex h-3 w-3 shrink-0 items-center justify-center animate-in fade-in-0 duration-200 ease-out",
className,
)}
>
<Loader2
aria-hidden="true"
className="h-3 w-3 animate-spin text-text-info"
/>
</span>
);
}
@ -52,7 +60,7 @@ export function SessionActivityIndicator({
role="status"
aria-label="Unread messages"
className={cn(
"absolute -right-0.5 -top-0.5 h-2.5 w-2.5 shrink-0 rounded-full border border-background bg-brand",
"absolute -right-0.5 -top-0.5 h-2 w-2 shrink-0 rounded-full border border-background bg-background-info transition-opacity duration-200 ease-out animate-in fade-in-0",
className,
)}
/>
@ -63,7 +71,10 @@ export function SessionActivityIndicator({
<span
role="status"
aria-label="Unread messages"
className={cn("h-2 w-2 shrink-0 rounded-full bg-brand", className)}
className={cn(
"h-1.5 w-1.5 shrink-0 rounded-full bg-background-info transition-opacity duration-200 ease-out animate-in fade-in-0",
className,
)}
/>
);
}

View file

@ -64,7 +64,7 @@ function CommandInput({
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden focus-visible:ring-0 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50",
"placeholder:text-placeholder flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden focus-visible:ring-0 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}

View file

@ -4,7 +4,7 @@ import { cn } from "@/shared/lib/cn";
const variantStyles = {
default: [
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input hover:border-border-input-hover focus-visible:border-ring focus-visible:ring-0 focus-visible:ring-offset-0 flex h-9 w-full min-w-0 rounded-input border bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"file:text-foreground placeholder:text-placeholder selection:bg-primary selection:text-primary-foreground border-input hover:border-border-input-hover focus-visible:border-ring focus-visible:ring-0 focus-visible:ring-offset-0 flex h-9 w-full min-w-0 rounded-input border bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
],
ghost: [

View file

@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-0 focus-visible:ring-offset-0 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"border-input placeholder:text-placeholder focus-visible:border-ring focus-visible:ring-0 focus-visible:ring-offset-0 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
{...props}