diff --git a/apps/desktop/src/renderer/components/chat/AgentChatComposer.test.tsx b/apps/desktop/src/renderer/components/chat/AgentChatComposer.test.tsx index 66c7cf6a3..49c81865d 100644 --- a/apps/desktop/src/renderer/components/chat/AgentChatComposer.test.tsx +++ b/apps/desktop/src/renderer/components/chat/AgentChatComposer.test.tsx @@ -1,7 +1,7 @@ /* @vitest-environment jsdom */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { cleanup, fireEvent, render, screen, waitFor, type RenderResult } from "@testing-library/react"; +import { cleanup, fireEvent, render, screen, waitFor, within, type RenderResult } from "@testing-library/react"; import type { ComponentProps } from "react"; import type { NormalizedLinearIssue } from "../../../shared/types"; import { AgentChatComposer } from "./AgentChatComposer"; @@ -317,6 +317,22 @@ describe("AgentChatComposer", () => { expect(onSendSteerNow).toHaveBeenCalledTimes(1); }); + it("keeps Claude steering options compact and concise", () => { + renderComposer({ + ...CLAUDE_STEER_OVERRIDES, + onSendSteerNow: vi.fn(), + onSendSteerInterrupt: vi.fn(), + }); + + fireEvent.click(screen.getByRole("button", { name: "More send options" })); + + const menu = screen.getByRole("menu", { name: "Send options" }); + expect(menu.style.width).toBe("240px"); + expect(menu.textContent).toContain("After the current tool step."); + expect(menu.textContent).toContain("When this turn finishes."); + expect(menu.textContent).toContain("Stop and redirect Claude now."); + }); + it("disables the active-turn send actions when the draft is whitespace-only", () => { const onSendSteerNow = vi.fn(); renderComposer({ @@ -677,6 +693,20 @@ describe("AgentChatComposer", () => { expect(screen.getByRole("option", { name: /Bypass/ })).toBeTruthy(); }); + it("uses a compact permission menu without a selected-mode header", () => { + renderComposer({ + sessionProvider: "claude", + modelId: "anthropic/claude-sonnet-5", + availableModelIds: ["anthropic/claude-sonnet-5"], + }); + + fireEvent.click(screen.getByRole("button", { name: "Claude permission mode" })); + + const listbox = screen.getByRole("listbox", { name: "Claude permission mode" }); + expect(listbox.style.width).toBe("240px"); + expect(within(listbox).queryByText("Mode", { exact: true })).toBeNull(); + }); + it.each(CAPTION_FREE_PERMISSION_CASES)( "renders title-only $provider permission rows", ({ triggerName, optionCount, overrides }) => { diff --git a/apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx b/apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx index 63036f831..5580a049a 100644 --- a/apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx +++ b/apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx @@ -530,6 +530,8 @@ const COMPOSER_PERMISSION_TRIGGER_CLASS = cn( "hover:border-violet-400/20 hover:bg-violet-500/[0.06] hover:text-fg", ); +const COMPOSER_COMPACT_MENU_WIDTH = 240; + type PermissionModeTone = "green" | "amber" | "blue" | "purple" | "red" | "slate"; type PermissionModeIconKind = "manual" | "auto" | "edit" | "plan" | "full" | "config" | "agent" | "agi"; @@ -715,7 +717,7 @@ function PermissionModePicker({ {open && ref.current ? createPortal( (() => { const rect = ref.current.getBoundingClientRect(); - const width = 284; + const width = COMPOSER_COMPACT_MENU_WIDTH; const left = Math.min(Math.max(8, rect.left), Math.max(8, window.innerWidth - width - 8)); return (
({ width, }} > -
- - - -
-
- Mode -
-
- {selectedOption.label} -
-
-
-
-
Faster diff --git a/apps/desktop/src/renderer/index.css b/apps/desktop/src/renderer/index.css index 690638f21..8470610f2 100644 --- a/apps/desktop/src/renderer/index.css +++ b/apps/desktop/src/renderer/index.css @@ -4134,9 +4134,16 @@ button:active, [role="button"]:active { } /* Model reasoning slider: progressive fill, active-tier pulse, and drag state. */ -@keyframes ade-reasoning-effort-word-pop { - 0% { opacity: 0; transform: translateY(3px); filter: blur(3px); } - 100% { opacity: 1; transform: translateY(0); filter: blur(0); } +@keyframes ade-reasoning-effort-word-roll-up { + 0% { opacity: 0; transform: translateY(65%) perspective(90px) rotateX(-78deg); filter: blur(2px); } + 72% { opacity: 1; transform: translateY(-4%) perspective(90px) rotateX(6deg); filter: blur(0); } + 100% { opacity: 1; transform: translateY(0) perspective(90px) rotateX(0); filter: blur(0); } +} + +@keyframes ade-reasoning-effort-word-roll-down { + 0% { opacity: 0; transform: translateY(-65%) perspective(90px) rotateX(78deg); filter: blur(2px); } + 72% { opacity: 1; transform: translateY(4%) perspective(90px) rotateX(-6deg); filter: blur(0); } + 100% { opacity: 1; transform: translateY(0) perspective(90px) rotateX(0); filter: blur(0); } } @keyframes ade-reasoning-fill-sheen { @@ -4151,7 +4158,16 @@ button:active, [role="button"]:active { } .ade-reasoning-effort-word { - animation: ade-reasoning-effort-word-pop 180ms cubic-bezier(0.2, 0.8, 0.2, 1); + display: inline-block; + transform-origin: center bottom; +} + +.ade-reasoning-effort-word[data-direction="up"] { + animation: ade-reasoning-effort-word-roll-up 160ms cubic-bezier(0.2, 0.8, 0.2, 1); +} + +.ade-reasoning-effort-word[data-direction="down"] { + animation: ade-reasoning-effort-word-roll-down 160ms cubic-bezier(0.2, 0.8, 0.2, 1); } .ade-reasoning-slider-fill { @@ -4187,6 +4203,8 @@ button:active, [role="button"]:active { @media (prefers-reduced-motion: reduce) { .ade-toast-enter { animation: none; } .ade-reasoning-effort-word, + .ade-reasoning-effort-word[data-direction="up"], + .ade-reasoning-effort-word[data-direction="down"], .ade-reasoning-slider-fill::after, .ade-reasoning-slider-fill-max { animation: none; diff --git a/docs/features/chat/README.md b/docs/features/chat/README.md index ceb94c6bc..7db065b3c 100644 --- a/docs/features/chat/README.md +++ b/docs/features/chat/README.md @@ -98,7 +98,7 @@ for its separate RPC, sync, storage, and UI contracts. | `apps/desktop/src/renderer/lib/claudeAuthPrompt.ts` | Renderer-side classifier for Claude logged-out / `/login`-required error text. Drives the header and sticky login CTAs; matches both Claude-first wording and ADE's own "Authentication failed for <model>" classified message. | | `apps/desktop/src/renderer/lib/openExternal.ts` | Renderer-side router for outbound URLs. Defines the `ADE_OPEN_BUILT_IN_BROWSER_EVENT` window event plus `openUrlInAdeBrowser(url)` and `openExternalUrl(url)`. `openUrlInAdeBrowser` dispatches the event (so any open `WorkSidebar` can flip to its Browser tab), then calls `window.ade.builtInBrowser.navigate({ url, newTab: true })`. Anything that is not a normal `http`/`https`/`about:blank` URL falls through to `window.ade.app.openExternal` (system browser). All in-renderer URL clicks (markdown links, lane-runtime open buttons, etc.) go through this helper so the user stays inside ADE. | | `apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx` | Composer UI: single-session prompt entry, attachments, model/permission controls, slash commands, pending input answering, and parallel launch slot configuration. During an active Claude turn, the split Send caret selects inline, after-turn, or interrupt delivery without sending; the primary button and Enter execute the chosen mode. Staged messages expose send-during-turn, interrupt, cancel, and Edit-back-to-composer actions. Permission popover rows keep only the mode title in the visible row (the explanation remains in the tooltip/title) for every provider-backed picker. Codex MCP elicitations show Allow once / Deny, conditionally show Always allow, and expose safe URL authorization through ADE's browser. Pasted/dropped image attachments show pending thumbnails while temp files save, and native Electron clipboard images read bytes through `ade.app.readClipboardImage` then write them through `ade.agentChat.saveTempAttachment` so remote-bound chats receive a runtime-readable attachment path. The launch-prompt clipboard helper is gated separately from prompt copying: `launchPromptClipboardEnabled` controls copying and `launchPromptClipboardNoticeEnabled` controls whether composer reminder text is shown. Orchestration model-selection pending inputs decode the full agent briefing metadata (`workDescription`, `filesHint`, `dependsOn`) so the picker can show what the lead is spawning without preselecting a recommended model. | -| `apps/desktop/src/renderer/components/shared/ModelPicker/ReasoningEffortPicker.tsx` | Shared reasoning slider. Supports pointer drag with nearest-tick snap, keyboard arrows/Home/End, a progressive filled gradient and active-tier pulse, GPT-5.6 labels (Light, Medium, High, Extra High, Max, and Ultra where supported), and an Ultra multi-agent usage note. Choosing or dragging to a tier leaves the popover open; outside click or Escape closes it. | +| `apps/desktop/src/renderer/components/shared/ModelPicker/ReasoningEffortPicker.tsx` | Shared reasoning slider. Supports pointer drag with nearest-tick snap, keyboard arrows/Home/End, a progressive filled gradient, and a directional roll transition for the active tier label, GPT-5.6 labels (Light, Medium, High, Extra High, Max, and Ultra where supported), and an Ultra multi-agent usage note. Choosing or dragging to a tier leaves the popover open; outside click or Escape closes it. | | `apps/desktop/src/renderer/components/chat/ChatModelSelectionPendingCard.tsx` | Pending-input card used when ADE asks the user to choose a model for a new or rerouted agent. It renders the agent briefing, touched files, run-after dependencies, provider/model controls, cancel/confirm states, and leaves the model unset until the user chooses one. | | `apps/desktop/src/renderer/components/chat/ChatCursorCloudPanel.tsx` | Side panel for Cursor Cloud (background agents): lists existing cloud agents and runs for the lane, lets the user open an existing cloud chat in ADE, archive/unarchive/cancel, and stream run output. Backed by `ade.ai.cursorCloud.*` IPC. | | `apps/desktop/src/renderer/components/chat/CursorCloudInlineLaunch.tsx` | Inline composer affordance for "Send to Cursor Cloud": picks repo + branch + Cursor Cloud-eligible model, optionally targeting a detected PR, and dispatches the prompt to a fresh cloud agent. |