diff --git a/apps/desktop/src/components/layout/SidebarShell.tsx b/apps/desktop/src/components/layout/SidebarShell.tsx
index 45fdd484..96806dce 100644
--- a/apps/desktop/src/components/layout/SidebarShell.tsx
+++ b/apps/desktop/src/components/layout/SidebarShell.tsx
@@ -137,7 +137,7 @@ function SidebarTabButton({
? "0 1px 2px rgb(0 0 0 / 0.12)"
: "none",
transition:
- "background-color 120ms ease, color 120ms ease, border-color 120ms ease, transform 120ms ease",
+ "background-color 140ms ease-out, color 140ms ease-out, border-color 140ms ease-out, transform 140ms cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 140ms ease-out",
}}
>
diff --git a/apps/desktop/src/features/ai/AgentsSidebarPanel.tsx b/apps/desktop/src/features/ai/AgentsSidebarPanel.tsx
index 5c798f01..bf5a60f8 100644
--- a/apps/desktop/src/features/ai/AgentsSidebarPanel.tsx
+++ b/apps/desktop/src/features/ai/AgentsSidebarPanel.tsx
@@ -741,12 +741,13 @@ export function AgentsSidebarPanel() {
}}
title="New chat"
aria-label="New chat"
- className="flex h-5 w-5 items-center justify-center rounded"
+ className="ub-chrome-btn flex h-5 w-5 cursor-pointer items-center justify-center rounded"
style={{
width: metrics.actionButtonSize,
height: metrics.actionButtonSize,
color: "var(--text-secondary)",
background: "transparent",
+ border: "1px solid transparent",
}}
>
openChatHistoryInWorkspace()}
title="Open chat history"
- className="rounded px-1.5 py-0.5 text-[10.5px]"
+ className="ub-chrome-btn cursor-pointer rounded px-1.5 py-0.5 text-[10.5px]"
style={{
color: "var(--text-secondary)",
background: "transparent",
+ border: "1px solid transparent",
fontSize: metrics.summaryFontSize,
}}
>
diff --git a/apps/desktop/src/features/ai/components/AIChatAgentControls.tsx b/apps/desktop/src/features/ai/components/AIChatAgentControls.tsx
index 5415c037..d4c5d4d2 100644
--- a/apps/desktop/src/features/ai/components/AIChatAgentControls.tsx
+++ b/apps/desktop/src/features/ai/components/AIChatAgentControls.tsx
@@ -144,24 +144,13 @@ function DropdownField({
rememberFocusedElement();
setOpen(true);
}}
- className="flex items-center gap-1 rounded-md px-2 py-1 text-xs"
+ className="nw-control-trigger flex cursor-pointer items-center gap-1 rounded-md px-2 py-1 text-xs"
+ data-open={open ? "true" : undefined}
style={{
color: "var(--text-secondary)",
backgroundColor: "transparent",
border: "none",
opacity: isDisabled ? 0.45 : 1,
- transition: "background-color 100ms ease, color 100ms ease",
- }}
- onMouseEnter={(e) => {
- if (isDisabled) return;
- e.currentTarget.style.backgroundColor =
- "color-mix(in srgb, var(--bg-tertiary) 80%, transparent)";
- e.currentTarget.style.color = "var(--text-primary)";
- }}
- onMouseLeave={(e) => {
- if (isDisabled) return;
- e.currentTarget.style.backgroundColor = "transparent";
- e.currentTarget.style.color = "var(--text-secondary)";
}}
title={label}
disabled={isDisabled}
diff --git a/apps/desktop/src/features/ai/components/AIChatComposer.tsx b/apps/desktop/src/features/ai/components/AIChatComposer.tsx
index 93b616e9..63240389 100644
--- a/apps/desktop/src/features/ai/components/AIChatComposer.tsx
+++ b/apps/desktop/src/features/ai/components/AIChatComposer.tsx
@@ -1082,6 +1082,12 @@ export function AIChatComposer({
const stopTransitionLabel = hasPendingSubmitAfterStop
? "Sending next message after stop..."
: "Stopping previous run...";
+ const stopButtonVisible = isStreaming || isStopping;
+ const stopButtonOpacity = !stopButtonVisible
+ ? 0
+ : disabled || isStopping
+ ? 0.4
+ : 1;
const closeMentionPicker = () => setMentionState(EMPTY_MENTION_STATE);
const closeSlashPicker = () => setSlashState(EMPTY_SLASH_STATE);
@@ -1672,8 +1678,12 @@ export function AIChatComposer({
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
+ strokeLinejoin="round"
>
-
+ {/* Collapse: inner brackets at (5,5) and (9,9)
+ with diagonals out to the outer corners — reads
+ as arrows pulling inward toward the centre. */}
+
) : (
+ {/* Expand: outer brackets at the corners with
+ diagonals pushing outward — arrows reaching
+ toward the corners. */}
)}
@@ -1945,24 +1959,91 @@ export function AIChatComposer({
minHeight: expanded ? 42 : undefined,
}}
>
-
- {isStopTransitionActive ? (
-
- {stopTransitionLabel}
-
- ) : null}
- {footer}
+
+
+ {footer}
+
+
+ {stopTransitionLabel}
+
+ {/* Stop is rendered unconditionally and placed before
+ Send in DOM order so Send stays anchored to the right
+ edge regardless of streaming state. Visibility toggles
+ via opacity + pointer-events for a smooth fade with no
+ layout shift. */}
+
+
+
+
- {(isStreaming || isStopping) && (
-
-
-
-
-
- )}
{
+ setComposerExpanded(false);
void chatActions.sendMessage(sessionId);
}}
onStop={() => {
diff --git a/apps/desktop/src/features/editor/EditorPaneBar.tsx b/apps/desktop/src/features/editor/EditorPaneBar.tsx
index 184d79af..54161b03 100644
--- a/apps/desktop/src/features/editor/EditorPaneBar.tsx
+++ b/apps/desktop/src/features/editor/EditorPaneBar.tsx
@@ -788,6 +788,9 @@ export function EditorPaneBar({ paneId, isFocused }: EditorPaneBarProps) {
width: 20,
height: 20,
color: "var(--text-secondary)",
+ // Match UnifiedBar tab close: sit closer to the
+ // right edge of the tab.
+ marginRight: -6,
}}
>
!disabled && onChange(!value)}
+ className="nw-settings-toggle"
style={{
width: 36,
height: 20,
@@ -93,7 +95,6 @@ function Toggle({
backgroundColor: value ? "var(--accent)" : "var(--bg-tertiary)",
position: "relative",
flexShrink: 0,
- transition: "background-color 150ms",
opacity: disabled ? 0.4 : 1,
}}
>
@@ -139,6 +140,8 @@ function SegmentedControl({
onChange(opt.value)}
+ data-active={active || undefined}
+ className="nw-settings-segment"
style={{
padding: "3px 10px",
borderRadius: 5,
@@ -156,7 +159,6 @@ function SegmentedControl({
? "0 1px 3px rgba(0,0,0,0.1)"
: "none",
fontWeight: active ? 500 : 400,
- transition: "all 100ms",
}}
>
{opt.label}
@@ -256,6 +258,8 @@ function SelectField({
type="button"
disabled={disabled}
onClick={() => setOpen((v) => !v)}
+ data-open={open ? "true" : undefined}
+ className="nw-settings-select"
style={{
display: "flex",
alignItems: "center",
@@ -345,6 +349,7 @@ function SelectField({
onChange(opt.value);
setOpen(false);
}}
+ className="nw-settings-select-item"
style={{
display: "block",
width: "100%",
@@ -362,14 +367,6 @@ function SelectField({
cursor: "pointer",
whiteSpace: "nowrap",
}}
- onMouseEnter={(e) => {
- e.currentTarget.style.backgroundColor =
- "var(--bg-tertiary)";
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.backgroundColor =
- "transparent";
- }}
>
{opt.label}
@@ -419,18 +416,23 @@ function NumberStepper({
}}
>
onChange(Math.max(min, value - step))}
+ className="nw-settings-stepper-btn"
style={{
width: 24,
height: 26,
border: "none",
background: "transparent",
- cursor: "pointer",
+ cursor: value <= min ? "not-allowed" : "pointer",
color: "var(--text-secondary)",
fontSize: 14,
display: "flex",
alignItems: "center",
justifyContent: "center",
+ opacity: value <= min ? 0.45 : 1,
}}
>
−
@@ -459,6 +461,7 @@ function NumberStepper({
inputRef.current?.blur();
}
}}
+ className="nw-settings-stepper-input"
style={{
width: 34,
textAlign: "center",
@@ -471,18 +474,23 @@ function NumberStepper({
}}
/>
= max}
onClick={() => onChange(Math.min(max, value + step))}
+ className="nw-settings-stepper-btn"
style={{
width: 24,
height: 26,
border: "none",
background: "transparent",
- cursor: "pointer",
+ cursor: value >= max ? "not-allowed" : "pointer",
color: "var(--text-secondary)",
fontSize: 14,
display: "flex",
alignItems: "center",
justifyContent: "center",
+ opacity: value >= max ? 0.45 : 1,
}}
>
+
@@ -595,6 +603,8 @@ function ThemePicker({
onChange(name)}
+ data-active={active || undefined}
+ className="nw-settings-theme-tile"
style={{
display: "flex",
flexDirection: "column",
@@ -607,7 +617,6 @@ function ThemePicker({
: "2px solid var(--border)",
background: "var(--bg-secondary)",
cursor: "pointer",
- transition: "border-color 150ms",
}}
>
{/* Color preview */}
diff --git a/apps/desktop/src/features/vault/VaultSwitcher.tsx b/apps/desktop/src/features/vault/VaultSwitcher.tsx
index a3d080cd..a4d4cd99 100644
--- a/apps/desktop/src/features/vault/VaultSwitcher.tsx
+++ b/apps/desktop/src/features/vault/VaultSwitcher.tsx
@@ -86,16 +86,12 @@ export function VaultSwitcher({
- (e.currentTarget.style.backgroundColor = "var(--bg-tertiary)")
- }
- onMouseLeave={(e) =>
- (e.currentTarget.style.backgroundColor = "transparent")
- }
>
{checked ? "✓" : ""}
@@ -126,14 +122,14 @@ export function VaultSwitcher({
style={{
position: "absolute",
bottom: "100%",
- left: 0,
- right: 0,
+ left: 8,
+ right: 8,
marginBottom: 4,
zIndex: 9999,
borderRadius: 8,
backgroundColor: "var(--bg-secondary)",
border: "1px solid var(--border)",
- boxShadow: "0 4px 20px rgba(0,0,0,0.3)",
+ boxShadow: "0 8px 24px rgba(0,0,0,0.35)",
padding: 4,
}}
>
@@ -301,15 +297,28 @@ export function VaultSwitcher({
payload: { path: vaultPath },
});
}}
- className="w-full flex items-center gap-2 px-3 py-3 text-xs"
- style={{ color: "var(--text-secondary)" }}
+ data-open={isOpen ? "true" : undefined}
+ className="nw-vault-trigger flex w-full cursor-pointer items-center gap-2 text-xs"
+ style={{
+ margin: "4px 8px",
+ width: "calc(100% - 16px)",
+ padding: "4px 8px",
+ borderRadius: 7,
+ border: "1px solid color-mix(in srgb, var(--border) 70%, transparent)",
+ background: "color-mix(in srgb, var(--bg-tertiary) 38%, transparent)",
+ color: "var(--text-secondary)",
+ }}
>