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
2 changes: 1 addition & 1 deletion apps/desktop/src/components/layout/SidebarShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}}
>
<SidebarTabIcon view={view} />
Expand Down
6 changes: 4 additions & 2 deletions apps/desktop/src/features/ai/AgentsSidebarPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}}
>
<svg
Expand All @@ -765,10 +766,11 @@ export function AgentsSidebarPanel() {
type="button"
onClick={() => 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,
}}
>
Expand Down
15 changes: 2 additions & 13 deletions apps/desktop/src/features/ai/components/AIChatAgentControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
146 changes: 97 additions & 49 deletions apps/desktop/src/features/ai/components/AIChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -1672,8 +1678,12 @@ export function AIChatComposer({
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M1 5V1h4M9 13h4V9M1 1l4 4M13 13l-4-4" />
{/* Collapse: inner brackets at (5,5) and (9,9)
with diagonals out to the outer corners — reads
as arrows pulling inward toward the centre. */}
<path d="M5 1V5H1M9 13V9H13M5 5L1 1M9 9L13 13" />
</svg>
) : (
<svg
Expand All @@ -1684,7 +1694,11 @@ export function AIChatComposer({
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
>
{/* Expand: outer brackets at the corners with
diagonals pushing outward — arrows reaching
toward the corners. */}
<path d="M9 1h4v4M5 13H1V9M13 1l-5 5M1 13l5-5" />
</svg>
)}
Expand Down Expand Up @@ -1945,24 +1959,91 @@ export function AIChatComposer({
minHeight: expanded ? 42 : undefined,
}}
>
<div className="min-w-0 flex-1">
{isStopTransitionActive ? (
<div
className="truncate px-1 pb-1 text-xs"
style={{
color: "var(--text-secondary)",
}}
>
{stopTransitionLabel}
</div>
) : null}
{footer}
<div
className="relative min-w-0 flex-1"
// Footer and transition label share the same slot via
// an opacity crossfade. Both are always mounted so the
// dropdowns do not flash on/off when the user stops a
// run or queues a follow-up message. minHeight matches
// the footer so the row never grows vertically.
style={{ minHeight: 24 }}
>
<div
style={{
opacity: isStopTransitionActive ? 0 : 1,
pointerEvents: isStopTransitionActive
? "none"
: "auto",
transition: "opacity 160ms ease-out",
}}
aria-hidden={isStopTransitionActive || undefined}
>
{footer}
</div>
<div
className="pointer-events-none absolute inset-0 flex items-center truncate px-1 text-xs"
style={{
color: "var(--text-secondary)",
opacity: isStopTransitionActive ? 1 : 0,
transition: "opacity 160ms ease-out",
}}
aria-hidden={!isStopTransitionActive || undefined}
>
{stopTransitionLabel}
</div>
</div>
{/* 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. */}
<button
type="button"
onClick={onStop}
disabled={!stopButtonVisible || disabled || isStopping}
aria-hidden={!stopButtonVisible || undefined}
tabIndex={stopButtonVisible ? 0 : -1}
className="nw-composer-action flex shrink-0 cursor-pointer items-center justify-center rounded-full"
style={{
width: 28,
height: 28,
color: "#fff",
backgroundColor: "#b91c1c",
border: "none",
opacity: stopButtonOpacity,
pointerEvents: stopButtonVisible
? "auto"
: "none",
// No overshoot on transform: the bouncy ease
// used elsewhere would briefly grow the button
// past scale 1 while it is also fading out,
// which reads as a flash. Smooth ease-out keeps
// the press feel without that interaction.
transition:
"transform 120ms cubic-bezier(0.2, 0.8, 0.2, 1), filter 120ms ease-out, box-shadow 120ms ease-out, background-color 150ms ease, opacity 180ms ease-out",
}}
aria-label={isStopping ? "Stopping" : "Stop"}
title={isStopping ? "Stopping" : "Stop"}
><svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="currentColor"
>
<rect
x="2"
y="2"
width="10"
height="10"
rx="2"
/>
</svg>
</button>
<button
type="button"
onClick={onSubmit}
disabled={!canSubmit}
className="flex shrink-0 items-center justify-center rounded-full"
className="nw-composer-action flex shrink-0 cursor-pointer items-center justify-center rounded-full"
style={{
width: 28,
height: 28,
Expand All @@ -1972,7 +2053,8 @@ export function AIChatComposer({
: "var(--accent)",
border: "none",
opacity: canSubmit ? 1 : 0.4,
transition: "all 0.15s ease",
transition:
"transform 120ms cubic-bezier(0.34, 1.56, 0.64, 1), filter 120ms ease-out, box-shadow 120ms ease-out, background-color 150ms ease, color 150ms ease, opacity 150ms ease",
}}
aria-label={
hasPendingSubmitAfterStop
Expand Down Expand Up @@ -2002,40 +2084,6 @@ export function AIChatComposer({
<path d="M8 12V4M4 7l4-3 4 3" />
</svg>
</button>
{(isStreaming || isStopping) && (
<button
type="button"
onClick={onStop}
disabled={disabled || isStopping}
className="flex shrink-0 items-center justify-center rounded-full"
style={{
width: 28,
height: 28,
color: "#fff",
backgroundColor: "#b91c1c",
border: "none",
opacity: disabled || isStopping ? 0.4 : 1,
transition: "all 0.15s ease",
}}
aria-label={isStopping ? "Stopping" : "Stop"}
title={isStopping ? "Stopping" : "Stop"}
>
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="currentColor"
>
<rect
x="2"
y="2"
width="10"
height="10"
rx="2"
/>
</svg>
</button>
)}
</div>
<AIChatMentionPicker
open={mentionState.open}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,7 @@ export function AIChatSessionView({ paneId }: AIChatSessionViewProps) {
chatActions.attachFolder(folderPath, name, sessionId);
}}
onSubmit={() => {
setComposerExpanded(false);
void chatActions.sendMessage(sessionId);
}}
onStop={() => {
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/features/editor/EditorPaneBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}}
>
<svg
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/features/editor/UnifiedBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1380,6 +1380,10 @@ export function UnifiedBar({ windowMode }: UnifiedBarProps) {
style={{
width: tabLayout.closeButtonSize,
height: tabLayout.closeButtonSize,
// Eat into the tab's right padding so the
// close button sits closer to the right edge
// without losing its clickable area.
marginRight: -6,
}}
>
<svg
Expand Down
Loading
Loading